<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>버러범의 개발이야기</title>
    <link>https://sbl133.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 5 Jul 2026 19:04:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>buru_bum</managingEditor>
    <item>
      <title>[spring] spring security를 이용한 JWT 로그인 RefreshToken도입</title>
      <link>https://sbl133.tistory.com/89</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RefreshToken 도입 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sbl133.tistory.com/88&quot;&gt;이전 포스팅&lt;/a&gt;에서 세션/쿠키 방식의 로그인에 문제점들을 해결하고자 Spring Security를 이용한 JWT 로그인을 구현해 보았다.&lt;br /&gt;하지만 JWT 통한 로그인 방식에는 토큰 탈취라는 보안상의 큰 문제가 있었다.&lt;br /&gt;공격자는 탈취한 토큰을 통해 서버와 통신을할 수 있고 서버는 해당 요청이 사용자의 정당한 요청인지 공격자의 요청인지 구분할 수 없었다.&lt;br /&gt;따라서 토큰에 유효시간을 두어 토큰이 탈취되더라도 일정 시간이 지나면 공격자가 서버와 통신할 수 없게 만들어야 했다.&lt;br /&gt;하지만 이 경우 사용자도 토큰이 만료되는 시점에 다시 로그인을 해야되는 불편함이 생기고 이를 방지하기위해 RefreshToken를 도입하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RefreshToken 도입 후 로그인 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token를 도입한 서비스의 사용자 인증절차는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 로그인을 하면 AccessToken과 RefreshToken를 발급해준다.&lt;/li&gt;
&lt;li&gt;서버는 데이터베이스에 두 토큰을 한 쌍으로 저장한다.&lt;/li&gt;
&lt;li&gt;사용자는 인가가 필요한 요청에 대해 AccessToken를 헤더에 실어 보냄으로써 서버로부터 인가를 받는다.&lt;/li&gt;
&lt;li&gt;AccessToken이 만료된경우 사용자는 RefreshToken를 통해 서버에게 토큰 재발급 요청을 한다.&lt;/li&gt;
&lt;li&gt;토큰 재발급 요청을 받은 서버는 AccessToken과 RefreshToken 각각 새로 만들어 새로운 한 쌍을 데이터베이스에 저장하고 사용자에게 발급한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AccessToken과 RefreshToken를 이런식으로 관리한다면 공격자에게 RefreshToken이 탈취당하더라도 다음과 같이 대처가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공격자가 RefreshToken를 통해 AccessToken를 발급 받으려는 경우 AccessToken의 유효기간이 만료되지 않았다면 서버에서는 RefreshToken이 탈취되었다고 간주하고 데이터베이스에서 해당 쌍의 Token를 삭제한다.&lt;br /&gt;이 경우 공격자와 사용자 모두 재로그인이 요구된다.&lt;/li&gt;
&lt;li&gt;공격자가 RefreshToken를 통해 AccessToken를 발급 받으려는 경우 AccessToken의 유효기간 또한 만료되었다면 공격자는 새로운 쌍의 토큰을 발급받을 수 있지만 사용자가 해당 서비스를 이용하는 순간 서버는 토큰 탈취를 감지할 수 있게된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 토큰을 이용한 로그인 전략은 스택오버플로우의 아래 링크를 참고하였다.&lt;br /&gt;&lt;a href=&quot;https://stackoverflow.com/questions/32060478/is-a-refresh-token-really-necessary-when-using-jwt-token-authentication&quot;&gt;https://stackoverflow.com/questions/32060478/is-a-refresh-token-really-necessary-when-using-jwt-token-authentication&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RefreshToken의 도입 이유와 전략에 대한 개략적인 설명은 끝났으니 이제 구현된 코드를 살펴보자&lt;br /&gt;이번 포스팅에서는 &lt;a href=&quot;https://sbl133.tistory.com/88&quot;&gt;이전 포스팅&lt;/a&gt;과 비교해서 RefreshToken를 도입하는 과정에서 달라진 코드만 살펴보겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;본격적으로 코드를 살펴보기 전에 먼저 AccessToken과 RefreshToken를 한 쌍으로 관리하기 위해 만든 Entity를 보자.&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TokenPair {

    @Id @GeneratedValue
    private Long id;
    private String accessToken;
    private String refreshToken;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_id&quot;, unique = true)
    private User user;

    public static TokenPair createTokenPair(String accessToken, String refreshToken, User user) {
        TokenPair tokenPair = new TokenPair();
        tokenPair.accessToken = accessToken;
        tokenPair.refreshToken = refreshToken;
        tokenPair.user = user;
        return tokenPair;
    }

    public void updateToken(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RefreshToken를 도입하면서 이전 포스팅과 달라진 부분은 크게 세 가지 이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Authentication 과정을 시큐리티 필터에서 MVC계층으로 옮김&lt;/li&gt;
&lt;li&gt;토큰의 효율적인 생산 및 관리를 위해 JwtUtils클래스 생성&lt;/li&gt;
&lt;li&gt;refreshToken를 통한 토큰 갱신요청인 POST: /refresh 요청을 받는 controller 및 service 메소드 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;먼저 AuthenticationFilter와 Authentication 성공 or 실패시 동작하는 AuthenticationSuccessHandlerCustom과 AuthenticationFailureHandlerCustom를 제거하고 추가한 코드를 살펴보자&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
public class UserController {
    // ...
    @PostMapping(&quot;/login&quot;)
    public LoginResponse login(@Valid @RequestBody LoginRequest request) {
        return userService.login(request);
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
    private final JwtUtiles jwtUtiles;
    private final AuthenticationManager authenticationManager;
    private final TokenPairRepository tokenPairRepository;
    // ...
    public LoginResponse login(LoginRequest request) {
        // authenticationManager 이용한 아이디 및 비밀번호 확인
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getLoginId(), request.getPassword()));

        // 인증된 객체 생성
        PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();

        // 인증 객체를 통해 tokenPair 생성
        String jwt = jwtUtils.createAccessToken(authentication);
        String refresh = jwtUtils.createRefreshToken(authentication);
        TokenPair tokenPair = TokenPair.createTokenPair(jwt, refresh, principal.getUser());

        // 기존 토큰이 있으면 수정, 없으면 생성
        tokenPairRepository.findByUserId(principal.getUser().getId())
                .ifPresentOrElse(
                        (findTokenPair) -&amp;gt; findTokenPair.updateToken(jwt, refresh),
                        () -&amp;gt; tokenPairRepository.save(tokenPair)
                );

        LoginResponse response = principal.getUser().getLoginInfo();
        response.setAccessToken(jwtUtils.addPrefix(jwt));
        response.setRefreshToken(jwtUtils.addPrefix(refresh));
        return response;
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 수정한 이유는 Authentication 부분을 security 필터에서 MVC계층으로 옮겨 Controller와 Service에 나누어 구현하기 위해서였다.&lt;br /&gt;이전부터 필터에서의 데이터베이스 조회 부분이 부자연스럽다고 생각했었지만, 단순 조회여서 transaction를 열지 않아도 되고 findById 수준의 조회는 성능 이슈에도 크게 영향을 미치지 않을것 같아 그대로 진행했었다.&lt;br /&gt;하지만 RefreshToken를 도입함으로써 Authentication 과정에서 transaction를 열어 dirty checking를 통한 update나 save 등&lt;br /&gt;높은 수준의 데이터베이스 상호작용이 요구되었기 때문에 해당 기능을 service 계층으로 옮기는것이 좀 더 자연스러워 보였다.&lt;br /&gt;여기서 주의할 점은 authenticationManger.authenticate()를 통해 사용자 인증시 인증에 통과하지 못하면 예외를 던지게 되는데,&lt;br /&gt;해당 예외는 AuthenticationEntryPointCustom에서 처리하기 때문에 해당 service에서는 신경쓰지 않아도 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;두 번째 큰 변화는 위 코드에서도 쓰인 JwtUtils 클래스의 추가이다. 코드는 아래와 같다.&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class JwtUtils {

    // HMAC512의 시크릿키
    @Value(&quot;${...}&quot;)
    private String secretKey;

    // accessToken 유효시간
    @Value(&quot;${...}&quot;)
    private int jwtExpirationInMs;

    // refreshToken 유효시간
    @Value(&quot;${...}&quot;)
    private int refreshExpirationDateInMs;
    // accessToken 생성 메서드
    public String createAccessToken(Authentication authentication) {
        PrincipalDetails userDetails = (PrincipalDetails) authentication.getPrincipal();
        String token = JWT.create()
                .withSubject(&quot;ILuvIt_AccessToken&quot;)
                .withExpiresAt(new Date(System.currentTimeMillis() + jwtExpirationInMs * 1000L))
                .withClaim(&quot;id&quot;, userDetails.getUser().getId())
                .sign(Algorithm.HMAC512(secretKey));
        return token;
    }
    // refreshToken 생성 메서드
    public String createRefreshToken(Authentication authentication) {
        PrincipalDetails userDetails = (PrincipalDetails) authentication.getPrincipal();
        String token = JWT.create()
                .withSubject(&quot;ILuvIt_RefreshToken&quot;)
                .withExpiresAt(new Date(System.currentTimeMillis() + refreshExpirationDateInMs * 1000L))
                .withClaim(&quot;id&quot;, userDetails.getUser().getId())
                .sign(Algorithm.HMAC512(secretKey));
        return token;
    }
    // 토큰에서 사용자 id 추출
    public Long getUserIdFromToken(String token) {
        DecodedJWT jwt = JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
        return jwt.getClaim(&quot;id&quot;).asLong();
    }
    // 토큰이 만료됐는지 check
    public Boolean isExpired(String token) {
        try {
            JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
            return false;
        } catch (TokenExpiredException e) {
            log.warn(&quot;[JwtVerificationException] token 기간 만료 : {}&quot;, e.getMessage());
            return true;
        } catch (JWTVerificationException e) {
            log.warn(&quot;[JWTVerificationException] token 파싱 실패 : {}&quot;, e.getMessage());
            return false;
        }
    }
    // 토큰이 유효한지 check
    public void validateToken(String token) {
        try {
            JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token);
        } catch (JWTVerificationException e) {
            log.warn(&quot;[JWTVerificationException] token 파싱 실패 : {}&quot;, e.getMessage());
            throw e;
        }
    }
    // 토큰 타입 명시
    public String addPrefix(String token) {
        return &quot;Bearer &quot; + token;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtUtils의 각 메서드의 역할은 주석으로 명시되어있다. 해당 클래스는 @Component를 이용하여 bean으로 등록함으로써 다른 빈에게 주입할 수 있게끔 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마지막으로 refreshToken를 갱신하는 부분이다.&lt;/h4&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// ...
public class UserController {
    // ...
    @PostMapping(&quot;/refresh&quot;)
    public LoginResponse refresh(@Valid @RequestBody TokenRefreshRequest request) throws IOException {
        LoginResponse response = userService.refresh(request);
        if (response != null) {
            return response;
        } else {
            throw new JWTVerificationException(&quot;유효하지 않은 시도입니다.&quot;);
        }
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// ...
public class UserService {
    // ...
    public LoginResponse refresh(TokenRefreshRequest request) {

        String requestRefreshTokenToken = request.getRefreshToken().replace(&quot;Bearer &quot;, &quot;&quot;);

        // 요청으로 받은 refreshToken 유효한지 확인
        jwtUtils.validateToken(requestRefreshTokenToken);

        // 이전에 받았던 refreshToken과 일치하는지 확인(tokenPair 유저당 하나로 유지)
        Long userId = jwtUtils.getUserIdFromToken(requestRefreshTokenToken);
        TokenPair findTokenPair = tokenPairRepository.findByUserIdWithUser(userId)
                .orElseThrow(() -&amp;gt; new JWTVerificationException(&quot;유효하지 않은 토큰입니다.&quot;));

        if (!requestRefreshTokenToken.equals(findTokenPair.getRefreshToken())) {
            throw new JWTVerificationException(&quot;유효하지 않은 토큰입니다.&quot;);
        }

        // 이전에 발급했던 AccessToken 만료되지 않았다면 refreshToken 탈취로 판단
        // TokenPair 삭제 -&amp;gt; 다시 로그인 해야됨
        if (jwtUtils.isExpired(findTokenPair.getAccessToken())) {
            // refreshToken 유효하고, AccessToken 정상적으로 Expired 상태일때
            PrincipalDetails principal = new PrincipalDetails(findTokenPair.getUser());
            Authentication authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());

            String jwt = jwtUtils.createAccessToken(authentication);
            String refresh = jwtUtils.createRefreshToken(authentication);
            findTokenPair.updateToken(jwt, refresh);

            LoginResponse response = principal.getUser().getLoginInfo();
            response.setAccessToken(jwtUtils.addPrefix(jwt));
            response.setRefreshToken(jwtUtils.addPrefix(refresh));
            return response;

        } else {
            // accessToken이 아직 만료되지 않은 상태 -&amp;gt; 토큰 탈취로 판단 -&amp;gt; delete tokenPair
            tokenPairRepository.delete(findTokenPair);
            return null;
        }
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;refreshToken과 함께 POST /refresh 요청을 받으면 먼저 요청으로 온 refreshToken의 유효성을 검사한다.&lt;br /&gt;유효성 검사에 통과하지 못한다면 예외를 던지고 해당 예외는 ControllerAdvice에서 ExceptionHandler를 통해 client에게 401에러(Unauthorized)를 응답으로 보낸다.&lt;br /&gt;해당 절차는 데이터베이스의 tokenPair와 비교해서 불일치 할때도 마찬가지로 적용된다.&lt;br /&gt;요청이 데이터베이스에 저장된 정보와도 일치하는지까지 확인했다면,&lt;br /&gt;이제는 요청으로 온 refreshToken과 쌍을 이루는 accessToken의 유효시간이 만료되었는지 검사한다.&lt;br /&gt;이때 아직 accessToken이 만료되지 않았다면 데이터베이스의 해당 tokenPair를 지우고 예외를 던져 사용자가 다시 로그인 하게끔 작동해야한다.&lt;br /&gt;여기서 주의를 기울였던 부분은 만약 delete(findTokenPair) 다음에 예외를 바로 던지게 되면&lt;br /&gt;transaction이 롤백되어 데이터베이스에 기존 정보가 남아있게 된다는 것이였다.&lt;br /&gt;따라서 service계층에서는 accessToken이 만료된 경우에만 response를 return 하고 accessToken이 만료되지 않은경우&lt;br /&gt;데이터베이스에 delete(findTokenPair)를 실행 한 후 null를 return하여 controller에서 예외를 던지게끔 설계하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 spring security를 이용한 JWT 로그인에 RefreshToken 도입 과정이였다.&lt;/p&gt;</description>
      <category>Spring</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/89</guid>
      <comments>https://sbl133.tistory.com/89#entry89comment</comments>
      <pubDate>Mon, 1 Aug 2022 01:51:52 +0900</pubDate>
    </item>
    <item>
      <title>[spring] spring security를 이용한 JWT 로그인</title>
      <link>https://sbl133.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존에는 세션/쿠키 방식으로 로그인을 구현하였지만 다중 서버환경에서의 서버간 세션공유문제, 앱으로 확장했을때 clinet의 환경 차이,&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Front-end에서 로컬환경으로 테스트할때 Back-end 서버와의 쿠키공유 불가 등 여러 문제점들로 인해 다른 방식의 로그인 구현이 요구되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;물론 다중 서버환경의 경우 세션클러스팅이나 세션키값을 DB에 저장하는 방식으로 문제 해결이 가능하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱으로의 확장 문제도 client단에서 어느정도 해결가능한 문제였지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;직면한 문제들을 좀 더 효율적으로 해결할 수 있는 방법은 JWT를 기반으로 로그인을 구현하는 것이라 생각하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 JWT로그인을 구현하게 되었고 인증/인가에서 많은 부분 도움을 주는 spring security를 함께 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 개략적인 흐름을 파악하기 위해서 SecurityConfig부터 살펴보자&lt;/p&gt;
&lt;pre id=&quot;code_1658834402543&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserRepository userRepository;
    private final CorsConfig corsConfig;
    private final AuthenticationConfiguration authenticationConfiguration;
    private final AccessDeniedHandler accessDeniedHandler;
    private final AuthenticationEntryPoint authenticationEntryPoint;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.addFilter(corsConfig.corsFilter())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .addFilter(jwtAuthenticationFilter())
                .addFilter(jwtAuthorizationFilter())
                .addFilterBefore(exceptionHandlerFilter(), LogoutFilter.class)
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint)
                .and()
                .authorizeRequests()
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                .antMatchers(&quot;/user/**&quot;)
                .access(&quot;hasRole('PARENT') or hasRole('TEACHER') or hasRole('DIRECTOR')&quot;)
                .antMatchers(&quot;/parent/**&quot;)
                .access(&quot;hasRole('PARENT')&quot;)
                .anyRequest().permitAll();
        return http.build();
    }

    @Bean ExceptionHandlerFilter exceptionHandlerFilter() {
        return new ExceptionHandlerFilter();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return new CustomAuthenticationFailureHandler();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManagerBean());
        jwtAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        jwtAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
        return jwtAuthenticationFilter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() throws Exception {
        return new JwtAuthorizationFilter(authenticationManagerBean(), userRepository);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존에는 WebSecurityConfigurerAdapter를 상속받아 SecurityConfig를 구현하였지만 최근에 WebSecurityConfigurerAdapter가 deprecated되면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;WebSecurityConfigurerAdapter의 메소드들을 override하는 방식이 아닌 @Bean을 통한 빈 등록을 이용해 구현할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 SecurityFilterChain를 반환하는 filterChain메소드를 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;filterChain 메소드에서는 HttpSecurity를 통해 spring security의 각종 설정들을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;http.csrf().disable();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;csrf().disable()를 통해 SpringSecurity에 default로 적용되어있는 csrf protection를 해제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이유는 현재 서버가 rest api 서버이고 인증정보로 JWT를 사용할 것이기 때문에 서버에 인증정보를 저장하지 않으므로 굳이 사용할 이유가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;http.addFilter(corsConfig.corsFilter())
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;corsConfig.corsFilter를 추가하는 이유는 뒤에서 자세히 설명하겠지만 cors설정을 위한것이라고 생각하고 넘어가자&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;JWT를 사용할 것이기 때문에 STATELESS 즉, 세션을 사용하지 않겠다는 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;.formLogin().disable()
.httpBasic().disable()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;formLogin().disable()은 Spring Security가 기본적으로 제공하는 formLogin 기능을 사용하지 않겠다는것을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;httpBasic().disable()은 매 요청마다 id, pwd를 보내는 방식으로 인증하는 httpBasic를 사용하지 않겠다는것을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;.addFilter(jwtAuthenticationFilter())
.addFilter(jwtAuthorizationFilter())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음은 jwtAuthenticationFilter와 jwtAuthorizationFilter를 추가하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 필터 모두 Spring Security에서 제공하는 flter를 상속받아 구현한것으로 구현 코드는 뒤에서 자세히 다루도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.addFilterBefore(exceptionHandlerFilter(), LogoutFilter.class)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;exceptionHandelrFilter는 filter에서 터지는 exception들을 효율적으로 관리하기 위해 만든 filter이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ExceptionResolver를 호출하는 DispatcherServlet 앞단에 filter들이 위치하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;filter들의 exception들을 처리할 수 있는 별도의 장치가 필요해 보여 구현하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;security에서 적용되는 filter중에 거의 제일 앞에 있는 LogoutFilter 앞에 적용시켜&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;filter에서 터지는 거의 모든 exception를 관리할 수 있도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(spring security를 지속적으로 공부하면서 알게된 사실이지만 인증/인가 수준에서 나올 수 있는 거의 모든 exception들을&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;처리하는 handler들이 security속에 다 구현돼 있어서 custom만 알맞게 한다면 직접 exceptionHandlerFilter를 만들 필요는 없는거 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인가 과정에서 JwtAuthorizationFilter가 JWT를 파싱하면서 토큰이 없거나 유효하지 않으면 authenticationEntryPoint에서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;권한 scope가 적절하지 않은 요청이면 AccessDeniedHandler에서 처리하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 클래스 모두 security에서 제공하는 interface를 custom한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.antMatchers(&quot;/user/**&quot;)
.access(&quot;hasRole('PARENT') or hasRole('TEACHER') or hasRole('DIRECTOR')&quot;)
.antMatchers(&quot;/parent/**&quot;)
.access(&quot;hasRole('PARENT')&quot;)
.anyRequest().permitAll();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;authorizeRequests() 밑으로는 어떤 요청들을 어떻게 처리할지 설정하는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;requestMatchers(CorsUtils::isPreFlightRequest).permitAll() 부분은 PreFlight 요청에 대해 모두 허용하겠다는 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;.antMatchers(&quot;/user/**&quot;) &lt;br /&gt;.access(&quot;hasRole('PARENT')&amp;nbsp;or&amp;nbsp;hasRole('TEACHER')&amp;nbsp;or&amp;nbsp;hasRole('DIRECTOR')&quot;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 부분은 &quot;/user/&quot;로 시작하는 모든 요청에 대해 PARENT 또는 TEACHER 또는 DIRECTOR 권한이 있어야만 응답하겠다는 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;filterChain 밑으로는 앞서 말한 직접 구현하는 filter들과 인증정보를 관리하는 AuthenticationManager의 빈 등록 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음으로 filter들을 구현하기전에 security에서 사용자 정보를 가져올때 사용하는 UserDetails와 그것을 반환하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UserDetailsService를 구현한 내용을 보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails {

    private final User user;

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        Collection&amp;lt;GrantedAuthority&amp;gt; authorities = new ArrayList&amp;lt;&amp;gt;();
        authorities.add(() -&amp;gt; (&quot;ROLE_&quot; + user.getAuth().toString()));
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLoginId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PrincipalDetails that = (PrincipalDetails) o;
        return Objects.equals(user, that.user);
    }

    @Override
    public int hashCode() {
        return Objects.hash(user);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서 주의해야할 점은 spring security에서 권한을 읽을때 prefix로 &quot;ROLE_&quot;이 붙는다 가정하고 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 spring security를 떼야되는? 상황이 올 수도 있고 사용자들의 권한을 관리하는데 prefix로 무언가 붙는것이 부자연스러워서&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;spring security가 사용자의 권한을 읽어오는 getAuthorities메소드를 구현할때 prefix로 &quot;ROLE_&quot;를 붙이게끔 하였다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // username이 loginId이다.
        User user = userRepository.findByLoginId(username)
                .orElseThrow(()-&amp;gt;new UsernameNotFoundException(&quot;존재하지 않는 아이디입니다.&quot;));
        return new PrincipalDetails(user);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 filter들의 구현내용들을 살펴보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;제일처음 살펴볼 filter는 JwtAuthenticationFilter이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 filter는 이름에서 알 수 있듯이 인증단계를 처리하는 필터로써 spring security에서 Authentication역할을 하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UsernamePasswordAuthenticationFilter를 상속받아 구현하였다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

        try {
            ObjectMapper om = new ObjectMapper();
            LoginRequest loginInfo = om.readValue(request.getInputStream(), LoginRequest.class);

            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginInfo.getLoginId(), loginInfo.getPassword());

            return authenticationManager.authenticate(authenticationToken);

        } catch (IOException e) {
             e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

        PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();

        String jwtToken = JWT.create()
                .withSubject(&quot;ILuvIt&quot;)
                .withExpiresAt(new Date(System.currentTimeMillis() + (60000 * 60 * 1)))
                .withClaim(&quot;id&quot;, principalDetails.getUser().getId())
                .sign(Algorithm.HMAC512(&quot;secretKey&quot;));
        response.addHeader(&quot;Authorization&quot;, &quot;Bearer &quot; + jwtToken);
        this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 구현하게 되면 client가 POST: /login 요청을 했을때 attemptAuthentication메소드가 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 메소드안에서는 ObjectMapper를 이용해 request에 담겨있는 사용자 정보를 DTO객체(loginRequest)로 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;spring security에서 loginRequest정보를 이용할수 있게 token화 시키고 해당 token으로 authenticationManager를 이용하여 인증을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;authenticationManager.authenticate 메소드는 PrincipaDetailsService에 loadByUsername 메소드를 호출하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;db정보와 요청 정보가 일치하는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 두 정보가 일치하지 않아 인증에 실패한다면 AuthenticationFailureHandler로 이동하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인증에 성공한다면 Authentication(인증정보)객체를 return하면서 successfulAuthentication메소드를 거쳐 AuthenticationSuccessHandler로 이동하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;successfulAuthentication에서는 attemptAuthentication에서 return한 인증정보를 기반으로 PrincipalDetails를 생성하여 JWT를 만드는데 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Authentication에 실패했을경우 실행되는 AuthenticationFailureHandler는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
public class AuthenticationFailureHandlerCustom implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.warn(&quot;Authentication Error: {}&quot;, exception.getMessage());
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단순히 서버에 로그를 남기고 Unauthorized(401) error를 client에게 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음은 Authentication에 성공했을경우 실행되는 AuthenticationSuccessHandler이다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public class AuthenticationSuccessHandlerCustom implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        LoginResponse loginResponse = principalDetails.getUser().getUserInfo();
        response.setContentType(&quot;application/json&quot;);
        response.getWriter().write(convertObjectToJson(loginResponse));
    }

    public String convertObjectToJson(Object object) throws JsonProcessingException {
        if (object == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(object);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서는 로그인 요청에 성공했을때 client에게 알맞게 응답할 수 있도록 인증정보를 이용하여 response body를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(LoginResponse 객체는 client가 필요로 하는 정보를 담은 DTO이다. JWT payload에 해당 정보들을 담을 수 있지만 일단은 response body에 해당 정보를 담게끔 구현하였다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 Authorization단계를 설명할 차례이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private final UserRepository userRepository;
    private AuthenticationManager authenticationManager;

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
        super(authenticationManager);
        this.userRepository = userRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

        String jwtHeader = request.getHeader(&quot;Authorization&quot;);

        if (jwtHeader == null || !jwtHeader.startsWith(&quot;Bearer&quot;)) {
            chain.doFilter(request, response);
            return;
        }

        try {
            String jwtToken = request.getHeader(&quot;Authorization&quot;).replace(&quot;Bearer &quot;, &quot;&quot;);
            DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(&quot;secretKey&quot;)).build().verify(jwtToken);
            Long id = decodedJWT.getClaim(&quot;id&quot;).asLong();
            User user = userRepository.findById(id).
                    orElseThrow(()-&amp;gt;new JWTVerificationException(&quot;유효하지 않은 토큰입니다.&quot;));

            PrincipalDetails principalDetails = new PrincipalDetails(user);
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    principalDetails, null, principalDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authentication);

        } catch (JWTVerificationException e) {
            log.warn(&quot;[JwtAuthorizationFilter] token 파싱 실패 : {}&quot;, e.getMessage());
        }
        chain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Authorization을 위한 JwtAuthorizationFilter는 BasicAuthenticationFilter를 상속받아 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;본래 BasicAuthenticationFilter는 httpBasic요청에 대한 인증 처리를 하는 필터이지만 securityConfig에서 disable하였기 때문에 상속받아 구현하여도 문제가 없어 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;물론 필터를 직접 새로 만들어서 addFilter하여도 되지만 있는 필터를 상속받아 구현하는게 훨씬 수월할거라 판단하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 securityConfig에서 빈 주입을 위해 필요한 생성자를 알맞게 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인가에 대한 전체적인 비즈니스 로직은 doFilterInternal에서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 헤더에 발급했던 토큰이 있는지 확인한다. 만약 토큰이 없다면 별도의 처리없이 해당 필터를 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 경우 해당 요청은 아무런 권한을 얻지 못하게 되고 만약 해당 요청에 특정 권한이 요구된다면 spring security의 다른 여러 필터들을 거쳐 결국 예외를 던져 AuthenticationEntryPoint에서 해당 예외를 처리하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;토큰이 존재한다면 해당 토큰을 확인하는 절차를 거친다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(&quot;secretKey&quot;)).build().verify(jwtToken);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 해당 토큰을 verify하는데 실패한다면 JWTVerificationException을 던지게 되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 요청 역시 토큰이 존재하지 않는 경우와 똑같이 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;토큰이 유효한 경우 토큰을 이용하여 인증객체를 만들고 아래와 같이 시큐리티 세션에 인증객체를 저장한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SecurityContextHolder.getContext().setAuthentication(authentication);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;세션에 인증정보를 저장한다면 Stateless하다는 JWT의 장점이 사라지는거 아닌가 물을 수 있겠지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;세션에 저장되는 인증정보는 해당 요청에 대한 권한 확인용으로만 사용되고&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 요청이 완료되면 더 이상 사용되지 않고 사라지기 때문에 걱정하는 문제는 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아무튼 spring security의 다른 필터들에서 시큐리티 세션에 저장된 인증정보와&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;securityConfig에서 설정한 각 요청들에 대한 필요 권한을 비교해보고&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;권한이 일치하지 않으면 예외를 던져 AccessDeniedHandler에서 처리하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 말한 검증들을 문제없이 통과한다면 해당 요청은 정상적으로 Controller까지 전달될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AccessDeniedHanlder와 AuthenticationEntryPoint를 커스텀한 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class AccessDeniedHandlerCustom implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.warn(&quot;[AccessDeniedHandlerCustom] Forbidden error : {}&quot;, accessDeniedException.getMessage());
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class AuthenticationEntryPointCustom implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.warn(&quot;[AuthenticationEntryPointCustom] Unauthorized error : {}&quot;, authException.getMessage());
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고로 상태코드 Unauthorized(401)는 이름에서 많이들 오해를 하지만 인가실패가 아닌 인증단계에서 에러가 있을때 주로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Forbidden(403)이 인가단계에서 실패하였을때 사용되는데 주로 사용자 권한의 scope와 요청에 필요한 권한이 일치하지 않을때 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;마지막으로 SecurityConfig에서 제일 먼저 addFilter한 CorsConfig의 corsFilter를 살펴보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(사실 이 부분에서 제일 많은시간을 삽질(?)했다...)&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class CorsConfig {

    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(List.of(&quot;http://localhost:3000&quot;));
        config.addAllowedHeader(&quot;*&quot;);
        config.addAllowedMethod(&quot;*&quot;);
        config.addExposedHeader(&quot;Authorization&quot;);


        source.registerCorsConfiguration(&quot;/**&quot;, config);
        return new CorsFilter(source);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;처음 SpringSecurity에서 Cors설정 부분을 보고 의아했던 사람도 있었을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통은 WebMvcConfigurer를 상속받아 구현하는 Configuration에서 addCorsMappings를 override 하는 방식으로 cors설정을 했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 SpringSecurity에서도 same-origin-policy를 검사하는 부분이 있어 보였고(사실 이부분을 디버깅하면서 찾아낼라 했지만 시간이 없어서 다음으로 미뤘다...) WebMvcConfigurer를 통해 cors설정을 하더라도 서블릿보다 앞단에 있는 필터에 해당 설정이 적용될리 만무했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서 SpringSecurity를 사용할 경우 CorsFilter를 통해 위 처럼 Cors설정을 따로 해줘야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개선해야할 점으로는 현재 AccessToken만료시 다시 로그인을 해야되는 문제가있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 문제는 추후에 RefreshToken 추가함으로써 개선할 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/88</guid>
      <comments>https://sbl133.tistory.com/88#entry88comment</comments>
      <pubDate>Thu, 28 Jul 2022 23:02:57 +0900</pubDate>
    </item>
    <item>
      <title>[spring] 관심사의 분리</title>
      <link>https://sbl133.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심사의 분리는 객체지향적 프로그래밍을 위한 매우 중요한 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 관심사의 분리는 스프링의 탄생배경과도 연관이 있기 때문에 한번쯤 정리하고 넘어가는것이 좋을거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말보다는 코드를 보면서 설명하는것이 효율적일거 같다. 우선 관심사의 분리가 일어나지 않은 코드를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member 객체를 저장하거나 조회하기 위해 사용되는 MemberRepository 인터페이스와 그것을 구현하는 두 종류의 구현체가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 각 객체들의 관계를 중심으로 설명할 것이므로 비지니스 로직이 중요한것이 아니여서 구현 코드는 생략하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1655180997460&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MemberRepository{
    // Member객체 저장
    void save(Member member);
    // memberId를 이용하여 Member객체 조회
    Member findById(Long memberId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1655181493196&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// in-memory 파일시스템을 기반으로 작동한다고 가정
public class MemroyMemberRepository implements MemberRepository{
    @Overrid
    public void save(Member member){
        ...
    }
    
    @Override
    public Member findById(Long memberId){
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1655181704030&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 데이터베이스를 기반으로 작동한다고 가정
public class DBMemberRepository implements MemberRepsoitory{
    @Override
    public void save(Member member){
        ...
    }
    
    @Override
    public Member findById(Long memberId){
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 MemberRepository를 이용하여 회원가입, 회원조회와 같은 서비스기능을 제공하는 MemberService 인터페이스와 그 구현체이다.&lt;/p&gt;
&lt;pre id=&quot;code_1655182523082&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MemberService{
    // 등록
    void join(Member member);
    // 조회
    Member findMember(Long memberId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1655182684625&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    @Override
    public void join(Member member){
        ...
    }
    
    @Override
    public Member findMember(Long memberId){
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 이제 본격적으로 관심사의 분리에 대해서 이야기를 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 MemberService의 구현체인 MemberServiceImpl은 MemoryMemberRepsitory를 구현체로 하는 MemberRepository를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 만약 MemberService를 데이터베이스 환경으로 구현하고 싶다면 우리는 MemberServiceImpl의 코드를 다음과 같이 변경할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1655183293536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberServiceImpl implements MemberService{
    // MemberRepository의 구현체를 DBMemberRepository로 바꿈
    private final MemberRepository memberRepository = new DBMemberRepository();
    
    @Override
    public void join(Member member){
        ...
    }
    
    @Override
    public Member findMember(Long memberId){
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇이 잘못되었는지 감이 오는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감이 잘 오지 않는다면 객체지향 설계 5원칙 SOLID중에서 SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), DIP(의존관계 역전 원칙)를 떠올려 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SRP - &quot;한 클래스는 하나의 책임만 가져야 한다&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 MemberServiceImpl은 의존 객체(MemberRepository)를 직접 생성하고 연결한뒤 로직을 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;의존 객체의 생성&quot;, &quot;연결&quot;, &quot;로직 구현&quot;이라는 &lt;b&gt;3가지 책임을 동시에&lt;/b&gt; 맡고있다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OCP - &quot;확장에는 열려 있으나 변경에는 닫혀 있어야 한다&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCP 관점에서 우리는 interface를 통해 여러 구현체를 만듦으로써 확장을 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 변경에는 닫혀 있는가?&amp;nbsp; 대답부터 하자면 그렇지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 MemberServiceImpl에서 MemberRepository를 할당할때 상황이 달라질때마다 구현체를 직접 할당하는 방식으로 &lt;b&gt;코드를 변경&lt;/b&gt;하여야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DIP - &quot;추상화에 의존해야지, 구체화에 의존하면 안된다&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DIP관점에서 봤을때 MemberServiceImpl는 MemberRepository 인터페이스에 의존하는것 처럼 보이지만 &lt;b&gt;실상은 그 구현체에 의존&lt;/b&gt;하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 MemberServiceImpl를 구현할때 상황이 달라질때마다 MemoryMemberRepository를 직접 할당하기도, DBMemberRepository를 직접 할당하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 현상이 벌어지는 이유는 기능구현의 책임을 가지고 있는 구현체가 의존관계 할당의 책임까지 맡고있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 같은 현상을 해결하기 위해서는 기능을 담당하는 구현체(MemberServiceImpl)는 역할구현에만 충실하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서로의 의존관계를 배분하는, 즉 의존관계 주입을 담당하는 또 다른 무언가가 있어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서 우리는 client 외부에서 작동하는 의존관계 할당이라는 책임을 맡는 &lt;b&gt;AppConfig&lt;/b&gt;라는 클래스를 만들어 보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1655186470626&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AppConfig{
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig를 활용하여 MemberServiceImpl를 리팩토링한 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1655186879058&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
    
    // 생성자를 통하여 MemberRepository의 구현체를 주입 받는다.
    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    
    @Override
    public void join(Member member){
        ...
    }
    
    @Override
    public Member findMember(Long memberId){
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우리는 AppConfig를 통해 외부에서 역할을 부여받음으로써 내부 구현체는 기능구현에만 책임을 갖도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조는 기존에 위반되었던 객체지향 설계원칙을 다음과 같은 이유로 지킬 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SRP (단일 책임 원칙)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 객체의 생성 및 의존관계 주입은 AppConfig가 담당하고 구현 객체는 해당 기능을 구현하는 책임만 담당함으로써 &lt;b&gt;단일 책임 원칙을 지켰다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OCP (개방-폐쇄 원칙)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberService를 예를 들면, 해당 인터페이스의 다른 구현체를 추가적으로 만드는것이 가능하므로 얼마든지 &lt;b&gt;확장 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 구현체를 확장해 나간다 하더라도 의존관계에 대한 관리는 AppConfig에서 하므로 구현체 입장에서의 &lt;b&gt;변경은 닫혀있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DIP (의존관계 역전 원칙)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberServiceImpl이 MemberRepository의 구현체에 의존하던것을 MemberRepository자체 &lt;b&gt;인터페이스에만 의존&lt;/b&gt;하도록 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신에 AppConfig가 MemberRepository의 구현체를 생성하여 MemberServiceImpl에 주입할 수 있도록 설계하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리해보자면 우리는 기존에 구현체에서 의존관계에 있는 객체를 직접 생성하고 맡은 기능을 수행하는등 여러가지 책임을 가지고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기존 방식은 객체지향적 프로그래밍 법칙에 많은 부분을 위반하고 있었고 이를 보완하기 위해 AppConfig라는 외부장치를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppConfig는 구현 객체들을 생성하고 연결시켜 주는 책임을 맡음으로써 기존 구현체들이 구현의 책임만 맡을 수 있도록 &lt;b&gt;관심사의 분리를 제공&lt;/b&gt;하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 외부에서 AppConfig가 프로그램에 대한 제어 흐름 권한을 가지고 구현 객체들을 생성 및 실행하는 경우 이것을 &lt;b&gt;제어의 역전(IoC)&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 AppConfig가 런타임 시점에 실행된다면 애플리케이션은 런타임에 구현 객체를 생성하고 의존관계를 연결하는데 이것을 &lt;b&gt;의존관계 주입(DI)&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 AppConfig는 IoC와 DI의 효과를 일으킨다고 말할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 관심사의 분리가 스프링의 탄생 배경과도 연관 있다고 말한 이유가 이 IoC와 DI에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링이 탄생하게 된 큰 이유중에 하나가 바로 IoC와 DI를 개발자들에게 좀 더 편하게 제공하려는 것이기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/87</guid>
      <comments>https://sbl133.tistory.com/87#entry87comment</comments>
      <pubDate>Tue, 14 Jun 2022 22:55:37 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 백준 2580번 스도쿠 c++</title>
      <link>https://sbl133.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/2580&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/2580&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650814096921&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2580번: 스도쿠&quot; data-og-description=&quot;스도쿠는 18세기 스위스 수학자가 만든 '라틴 사각형'이랑 퍼즐에서 유래한 것으로 현재 많은 인기를 누리고 있다. 이 게임은 아래 그림과 같이 가로, 세로 각각 9개씩 총 81개의 작은 칸으로 이루&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2580&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2580&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bESXAO/hyN9TdoUsH/2DFTb0QKH2nKGgm78HnrmK/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480,https://scrap.kakaocdn.net/dn/efiP3Z/hyN9UwBGpj/CndmvaJwEQkY2J92eizDSk/img.jpg?width=480&amp;amp;height=460&amp;amp;face=0_0_480_460,https://scrap.kakaocdn.net/dn/c1i0Bb/hyN9PIQyA6/iw50Ork0nKBMfYxCBBz7Z1/img.jpg?width=480&amp;amp;height=460&amp;amp;face=0_0_480_460&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2580&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2580&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bESXAO/hyN9TdoUsH/2DFTb0QKH2nKGgm78HnrmK/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480,https://scrap.kakaocdn.net/dn/efiP3Z/hyN9UwBGpj/CndmvaJwEQkY2J92eizDSk/img.jpg?width=480&amp;amp;height=460&amp;amp;face=0_0_480_460,https://scrap.kakaocdn.net/dn/c1i0Bb/hyN9PIQyA6/iw50Ork0nKBMfYxCBBz7Z1/img.jpg?width=480&amp;amp;height=460&amp;amp;face=0_0_480_460');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2580번: 스도쿠&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스도쿠는 18세기 스위스 수학자가 만든 '라틴 사각형'이랑 퍼즐에서 유래한 것으로 현재 많은 인기를 누리고 있다. 이 게임은 아래 그림과 같이 가로, 세로 각각 9개씩 총 81개의 작은 칸으로 이루&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스도쿠는 아래와 같은 규칙으로 빈칸을 채워나가는 게임이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 각각의 가로줄과 세로줄에는 1부터 9까지의 숫자가 한 번씩만 나타나야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 굵은 선으로 구분되어 있는 3*3 정사각형 안에도 1부터 9까지의 숫자가 한 번씩만 나타나야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력될때 빈칸은 0으로 입력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력으로 스도쿠 문제가 주어졌을때 알맞게 빈칸을 채운 9*9 행렬을 출력하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script src=&quot;https://gist.github.com/sbl133/02c2cb61389abcac71fc0a1ebe88dab8.js&quot;&gt;&lt;/script&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 원본: &lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%232580.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/sbl133/BOJ/blob/main/%232580.cpp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650814330096&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sbl133/BOJ&quot; data-og-description=&quot;Contribute to sbl133/BOJ development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%232580.cpp&quot; data-og-url=&quot;https://github.com/sbl133/BOJ&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/8eNvC/hyN9PIQL0W/f4PS3ZrjRMkp8rdQAqqyL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%232580.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%232580.cpp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/8eNvC/hyN9PIQL0W/f4PS3ZrjRMkp8rdQAqqyL0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sbl133/BOJ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sbl133/BOJ development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;36&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9XiVJ/btrAh9a1qs5/BXaCxuYD1YfHHM3fzTxS9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9XiVJ/btrAh9a1qs5/BXaCxuYD1YfHHM3fzTxS9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9XiVJ/btrAh9a1qs5/BXaCxuYD1YfHHM3fzTxS9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9XiVJ%2FbtrAh9a1qs5%2FBXaCxuYD1YfHHM3fzTxS9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1142&quot; height=&quot;36&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;36&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Algorithm/BOJ</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/86</guid>
      <comments>https://sbl133.tistory.com/86#entry86comment</comments>
      <pubDate>Mon, 25 Apr 2022 00:32:41 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 백준 1987번 알파벳 c++</title>
      <link>https://sbl133.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/1987&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/1987&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650293864619&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1987번: 알파벳&quot; data-og-description=&quot;세로 R칸, 가로 C칸으로 된 표 모양의 보드가 있다. 보드의 각 칸에는 대문자 알파벳이 하나씩 적혀 있고, 좌측 상단 칸 (1행 1열) 에는 말이 놓여 있다. 말은 상하좌우로 인접한 네 칸 중의 한 칸으&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1987&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1987&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WKeXZ/hyN5GSDUPI/ySCxsWdNLqOojMU00xjdHk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1987&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1987&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WKeXZ/hyN5GSDUPI/ySCxsWdNLqOojMU00xjdHk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1987번: 알파벳&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;세로 R칸, 가로 C칸으로 된 표 모양의 보드가 있다. 보드의 각 칸에는 대문자 알파벳이 하나씩 적혀 있고, 좌측 상단 칸 (1행 1열) 에는 말이 놓여 있다. 말은 상하좌우로 인접한 네 칸 중의 한 칸으&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R * C 크기의 표 모양의 보드가 알파벳으로 이뤄진채 주어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말은 보드의 좌측 상단에서 시작해 상하좌우 4방향으로 한 칸씩 이동 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 새로 이동한 칸에 적혀 있는 알파벳은 지금까지 지나온 모든 칸에 적혀 있는 알파벳과 달라야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 말이 최대한 몇칸을 지날 수 있는지 구하는것이 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 dfs를 이용하여 문제를 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한점은 이미 지나온 경로에 있는 알파벳을 따로 저장하여 앞으로의 경로와 비교해야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 지나온 알파벳을 비트마스크 형태로 저장하여 다음 경로의 알파벳과 bit단위 비교를 하여 다음 경로가 유효한지 판단하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script src=&quot;https://gist.github.com/sbl133/6bff6f2d032349465f7c27422294050e.js&quot;&gt;&lt;/script&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 원본 : &lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%231987.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/sbl133/BOJ/blob/main/%231987.cpp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650294212951&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sbl133/BOJ&quot; data-og-description=&quot;Contribute to sbl133/BOJ development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%231987.cpp&quot; data-og-url=&quot;https://github.com/sbl133/BOJ&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Mqr4s/hyN5DInC5V/LjREcU3eBTNfRANcj5vgf0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%231987.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%231987.cpp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Mqr4s/hyN5DInC5V/LjREcU3eBTNfRANcj5vgf0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sbl133/BOJ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sbl133/BOJ development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pKm90/btrzMJQok3E/X59lA8kBseo1Y4gHuiDeQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pKm90/btrzMJQok3E/X59lA8kBseo1Y4gHuiDeQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pKm90/btrzMJQok3E/X59lA8kBseo1Y4gHuiDeQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpKm90%2FbtrzMJQok3E%2FX59lA8kBseo1Y4gHuiDeQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1143&quot; height=&quot;37&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Algorithm/BOJ</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/85</guid>
      <comments>https://sbl133.tistory.com/85#entry85comment</comments>
      <pubDate>Tue, 19 Apr 2022 00:04:01 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 백준 1011번 Fly me to the Alpha Centauri c++</title>
      <link>https://sbl133.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/1011&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/1011&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650182730718&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1011번: Fly me to the Alpha Centauri&quot; data-og-description=&quot;우현이는 어린 시절, 지구 외의 다른 행성에서도 인류들이 살아갈 수 있는 미래가 오리라 믿었다. 그리고 그가 지구라는 세상에 발을 내려 놓은 지 23년이 지난 지금, 세계 최연소 ASNA 우주 비행&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1011&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1011&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fb9A2/hyN35yF2Oe/7SXHyqiW71Co4oV08TSe8k/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1011&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1011&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fb9A2/hyN35yF2Oe/7SXHyqiW71Co4oV08TSe8k/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1011번: Fly me to the Alpha Centauri&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;우현이는 어린 시절, 지구 외의 다른 행성에서도 인류들이 살아갈 수 있는 미래가 오리라 믿었다. 그리고 그가 지구라는 세상에 발을 내려 놓은 지 23년이 지난 지금, 세계 최연소 ASNA 우주 비행&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출발점에서 도착점까지 이동장치를 이용하여 이동한다 했을때 이동 규칙은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 직전 이동거리가 k광년이면 이번에 이동 가능한 거리는 k-1, k, k+1 셋 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음수 혹은 0거리만큼의 이동은 의미가 없다(이동 불가능 하다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 규칙으로 이동한다 했을때 공간이동 장치 작동 횟수의 최소값을 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 종류의 문제는 일단 가까운 거리부터 어떤방식으로 이동해야 가장 빨리 갈 수 있는지를 나열해 보면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나열하다 보니깐 다음과 같은 규칙을 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 1번 쓰는 경우 갈 수 있는 거리 : 1(1) - 1개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 2번 쓰는 경우 갈 수 있는 거리 : 2(11) - 1개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 3번 쓰는 경우 갈 수 있는 거리 : 3(111), 4(121) - 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 4번 쓰는 경우 갈 수 있는 거리 : 5(1211), 6(1221) - 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 5번 쓰는 경우 갈 수 있는 거리 : 7(12211), 8(12221), 9(12321) - 3개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 5번 쓰는 경우 갈 수 있는 거리 : 10, 11, 12 - 3개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 6번 쓰는 경우 갈 수 있는 거리 : 13, 14, 15, 16 - 4개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 7번 쓰는 경우 갈 수 있는 거리 : 4개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 장치를 8번 쓰는 경우 갈 수 있는 거리 : 5개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장치를 2번씩 더 쓸때마다 갈 수 있는 거리가 1개씩 더 늘어나는것을 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 규칙을 토대로 프로그래밍을 하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script src=&quot;https://gist.github.com/sbl133/a0a71be1c86ca368c228eac4f8afeb8a.js&quot;&gt;&lt;/script&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 원본 : &lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%231101.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/sbl133/BOJ/blob/main/%231101.cpp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650184989219&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sbl133/BOJ&quot; data-og-description=&quot;Contribute to sbl133/BOJ development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%231101.cpp&quot; data-og-url=&quot;https://github.com/sbl133/BOJ&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/EtS70/hyN3TyjiZw/EVVQPqrKd1fqMgnIO6iD10/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%231101.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%231101.cpp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/EtS70/hyN3TyjiZw/EVVQPqrKd1fqMgnIO6iD10/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sbl133/BOJ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sbl133/BOJ development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;36&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bur4m/btrzx8kjQuY/SRe6WnHl55Z6Vl3OdDymKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bur4m/btrzx8kjQuY/SRe6WnHl55Z6Vl3OdDymKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bur4m/btrzx8kjQuY/SRe6WnHl55Z6Vl3OdDymKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBur4m%2Fbtrzx8kjQuY%2FSRe6WnHl55Z6Vl3OdDymKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1145&quot; height=&quot;36&quot; data-origin-width=&quot;1145&quot; data-origin-height=&quot;36&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Algorithm/BOJ</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/84</guid>
      <comments>https://sbl133.tistory.com/84#entry84comment</comments>
      <pubDate>Sun, 17 Apr 2022 17:43:43 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 백준 2206번 벽 부수고 이동하기 c++</title>
      <link>https://sbl133.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/2206&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/2206&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650119362783&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2206번: 벽 부수고 이동하기&quot; data-og-description=&quot;N&amp;times;M의 행렬로 표현되는 맵이 있다. 맵에서 0은 이동할 수 있는 곳을 나타내고, 1은 이동할 수 없는 벽이 있는 곳을 나타낸다. 당신은 (1, 1)에서 (N, M)의 위치까지 이동하려 하는데, 이때 최단 경로&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2206&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2206&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dOlC4a/hyN31hFGae/fFgA0YuvppWxk8yoP7YFY1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2206&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2206&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dOlC4a/hyN31hFGae/fFgA0YuvppWxk8yoP7YFY1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2206번: 벽 부수고 이동하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;N&amp;times;M의 행렬로 표현되는 맵이 있다. 맵에서 0은 이동할 수 있는 곳을 나타내고, 1은 이동할 수 없는 벽이 있는 곳을 나타낸다. 당신은 (1, 1)에서 (N, M)의 위치까지 이동하려 하는데, 이때 최단 경로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N * M 크기의 맵이 있다. 맵에서 벽은 1, 벽이 없는 곳은 0으로 표현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1,1)에서 (N,M) 위치까지 상하좌우로 한칸씩 이동면서 최단경로를 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 벽을 부수고 이동하는 경우 경로가 더 짧아진다면, 벽을 한 개 까지 부수고 이동하여 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfs를 이용하면 문제를 해결할 수 있다. 이때 이미 지나온 곳을 다시 지나는것을 막기위해 discover배열을 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 벽을 부술 수 있는 상태인지 이미 벽을 부순 상태인지에 따라 이동할 수 있는 경로가 달라지므로 discover를 이차원이 아닌 3차원 배열로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/sbl133/9fd09410d479b2c5282e91711ea7c6c6.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드원본: &lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%232206.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/sbl133/BOJ/blob/main/%232206.cpp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1650120872804&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sbl133/BOJ&quot; data-og-description=&quot;Contribute to sbl133/BOJ development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%232206.cpp&quot; data-og-url=&quot;https://github.com/sbl133/BOJ&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/U8JNh/hyN30cqXCQ/kdFKiA2PLPNhzrf0nlGPj0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%232206.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%232206.cpp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/U8JNh/hyN30cqXCQ/kdFKiA2PLPNhzrf0nlGPj0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sbl133/BOJ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sbl133/BOJ development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1141&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCRaKR/btrzwfwr6Ga/M5DN5QLCh8yRDarxL7uOS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCRaKR/btrzwfwr6Ga/M5DN5QLCh8yRDarxL7uOS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCRaKR/btrzwfwr6Ga/M5DN5QLCh8yRDarxL7uOS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCRaKR%2Fbtrzwfwr6Ga%2FM5DN5QLCh8yRDarxL7uOS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1141&quot; height=&quot;37&quot; data-origin-width=&quot;1141&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Algorithm/BOJ</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/83</guid>
      <comments>https://sbl133.tistory.com/83#entry83comment</comments>
      <pubDate>Sat, 16 Apr 2022 23:55:08 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ] 백준 7576번 토마토 c++</title>
      <link>https://sbl133.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;BOJ 문제 링크: &lt;a href=&quot;https://www.acmicpc.net/problem/7576&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/7576&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649490029200&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;7576번: 토마토&quot; data-og-description=&quot;첫 줄에는 상자의 크기를 나타내는 두 정수 M,N이 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 &amp;le; M,N &amp;le; 1,000 이다. 둘째 줄부터는 하나의 상자에 저장된 토마토&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/7576&quot; data-og-url=&quot;https://www.acmicpc.net/problem/7576&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ydoAf/hyNYOJ3u0h/emlqb4APSKPkPyJUpwXQYk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480,https://scrap.kakaocdn.net/dn/jaqC3/hyNYPWuAgD/WHBd1yYYODcoCcSLuREn51/img.jpg?width=430&amp;amp;height=388&amp;amp;face=0_0_430_388&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/7576&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/7576&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ydoAf/hyNYOJ3u0h/emlqb4APSKPkPyJUpwXQYk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480,https://scrap.kakaocdn.net/dn/jaqC3/hyNYPWuAgD/WHBd1yYYODcoCcSLuREn51/img.jpg?width=430&amp;amp;height=388&amp;amp;face=0_0_430_388');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;7576번: 토마토&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫 줄에는 상자의 크기를 나타내는 두 정수 M,N이 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 &amp;le; M,N &amp;le; 1,000 이다. 둘째 줄부터는 하나의 상자에 저장된 토마토&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N * M 크기의 배열 형태로 격자모양을 한 토마토 상자의 정보가 주어진다. 각 칸에는 0, 1, -1이 올 수있는데 각각 익지않은 토마토, 익은 토마토, 빈칸을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;익은 토마토는 앞, 뒤, 좌, 우 4방향으로 익지 않은 토마토에게 영향을줘 다음날 익게만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 조건 속에서 토마토들의 정보가 주어졌을때 며칠이 지나면 토마토들이 모두 익는지 그 최소 일수를 구하면 되는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 토마토들이 전부 익는 경우가 없으면 -1를 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfs를 이용하면 문제를 풀 수 있지만 날짜를 계산하기 위해서는 추가적인 로직이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해서 앞으로 익을 토마토들의 목록을 저장하는 queue에 날짜구분자를 삽입하여 날짜를 구분할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script src=&quot;https://gist.github.com/sbl133/b625b42b802a5554bdd1a6de3dffd202.js&quot;&gt;&lt;/script&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 원본: &lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%237576.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/sbl133/BOJ/blob/main/%237576.cpp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649490911643&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sbl133/BOJ&quot; data-og-description=&quot;Contribute to sbl133/BOJ development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%237576.cpp&quot; data-og-url=&quot;https://github.com/sbl133/BOJ&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNilVO/hyNYDPpUje/xQ4Bk1gRHtjdvs3Jp0nB71/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sbl133/BOJ/blob/main/%237576.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sbl133/BOJ/blob/main/%237576.cpp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNilVO/hyNYDPpUje/xQ4Bk1gRHtjdvs3Jp0nB71/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sbl133/BOJ&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sbl133/BOJ development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;34&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVq9Km/btryPAO2Aww/lZoCuMFHgO8Dg5KfiskrUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVq9Km/btryPAO2Aww/lZoCuMFHgO8Dg5KfiskrUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVq9Km/btryPAO2Aww/lZoCuMFHgO8Dg5KfiskrUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVq9Km%2FbtryPAO2Aww%2FlZoCuMFHgO8Dg5KfiskrUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1138&quot; height=&quot;34&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;34&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Algorithm/BOJ</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/82</guid>
      <comments>https://sbl133.tistory.com/82#entry82comment</comments>
      <pubDate>Sat, 9 Apr 2022 16:55:45 +0900</pubDate>
    </item>
    <item>
      <title>algospot 여행 경로 정하기 (문제 ID: TPATH) c++</title>
      <link>https://sbl133.tistory.com/81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;알고스팟 문제 링크: &lt;a href=&quot;https://algospot.com/judge/problem/read/TPATH&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://algospot.com/judge/problem/read/TPATH&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649330513687&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	TPATH&quot; data-og-description=&quot;여행 경로 정하기 문제 정보 문제 당신은 철도망을 이용해 한 도시에서 다른 도시까지 여행을 하고 싶다. 철도망은 여러 개의 역들과 그들을 잇는 노선으로 구성되어 있다. 각 구간별로 철도의 &quot; data-og-host=&quot;algospot.com&quot; data-og-source-url=&quot;https://algospot.com/judge/problem/read/TPATH&quot; data-og-url=&quot;https://algospot.com/judge/problem/read/TPATH&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/beVKVu/hyNXvxkK6w/2I0vp6L0lZhwaDUCdo86f0/img.png?width=629&amp;amp;height=249&amp;amp;face=0_0_629_249&quot;&gt;&lt;a href=&quot;https://algospot.com/judge/problem/read/TPATH&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://algospot.com/judge/problem/read/TPATH&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/beVKVu/hyNXvxkK6w/2I0vp6L0lZhwaDUCdo86f0/img.png?width=629&amp;amp;height=249&amp;amp;face=0_0_629_249');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;algospot.com :: TPATH&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;여행 경로 정하기 문제 정보 문제 당신은 철도망을 이용해 한 도시에서 다른 도시까지 여행을 하고 싶다. 철도망은 여러 개의 역들과 그들을 잇는 노선으로 구성되어 있다. 각 구간별로 철도의&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 도시들을 잇는 철도망이 주어질 때, 0번 도시에서 출발해서 V-1번 도시에 도착하는 경로들 중 최대 운행 속도와 최소 운행 속도의 차를 최소화하는 경로를 찾는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0번 도시와 V-1번 도시가 연결될 때까지 하한을 높여가며 다음과 같은 동작을 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하한을 두고 kruscal알고리즘을 이용하여 하한이상의 가중치를 가진 철도부터 가중치 순서대로 그래프에 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 kruscal의 시간복잡도를 지배하는 것은 edges를 가중치의 오름차순으로 정렬하는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 철도의 가중치가 변하지는 않으므로 edges를 정렬하는 작업을 선행처리한 후 나머지 부분만 하한을 높여가며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복하면 기존의 kruscal알고리즘을 edges.size번 반복하는 것보다 빠르게 문제를 해결 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/sbl133/4eeb7e0b4d5902b069838040f7656ecd.js&quot;&gt;&lt;/script&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 원본: &lt;a href=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/TPATH.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/TPATH.cpp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649331567505&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sbl133/JongmanBook&quot; data-og-description=&quot;Contribute to sbl133/JongmanBook development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/TPATH.cpp&quot; data-og-url=&quot;https://github.com/sbl133/JongmanBook&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b29qfr/hyNXqQjbNH/BC5IR7Oe3A6Rkx23ZlDkVK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/TPATH.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/TPATH.cpp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b29qfr/hyNXqQjbNH/BC5IR7Oe3A6Rkx23ZlDkVK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sbl133/JongmanBook&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sbl133/JongmanBook development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;35&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deCmPU/btryKWwl8sq/Ev3nNFs4vO2d8HcGIZLA10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deCmPU/btryKWwl8sq/Ev3nNFs4vO2d8HcGIZLA10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deCmPU/btryKWwl8sq/Ev3nNFs4vO2d8HcGIZLA10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeCmPU%2FbtryKWwl8sq%2FEv3nNFs4vO2d8HcGIZLA10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;35&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;35&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reference: 프로그래밍 대회에서 배우는 알고리즘 문제해결전략2&lt;/p&gt;</description>
      <category>Algorithm/algospot</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/81</guid>
      <comments>https://sbl133.tistory.com/81#entry81comment</comments>
      <pubDate>Thu, 7 Apr 2022 20:39:45 +0900</pubDate>
    </item>
    <item>
      <title>algospot 근거리 네트워크 (문제 ID: LAN) c++</title>
      <link>https://sbl133.tistory.com/80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;알고스팟 문제 링크: &lt;a href=&quot;https://algospot.com/judge/problem/read/LAN&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://algospot.com/judge/problem/read/LAN&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649250916434&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	LAN&quot; data-og-description=&quot;근거리 네트워크 문제 정보 문제 근거리 네트워크(LAN, Local Area Network)는 가정, 학교, 혹은 회사 등의 제한된 공간 내의 컴퓨터들을 연결하는 네트워크입니다. 알고스팟 대학교에서는 캠퍼스의 일&quot; data-og-host=&quot;algospot.com&quot; data-og-source-url=&quot;https://algospot.com/judge/problem/read/LAN&quot; data-og-url=&quot;https://algospot.com/judge/problem/read/LAN&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://algospot.com/judge/problem/read/LAN&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://algospot.com/judge/problem/read/LAN&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;algospot.com :: LAN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;근거리 네트워크 문제 정보 문제 근거리 네트워크(LAN, Local Area Network)는 가정, 학교, 혹은 회사 등의 제한된 공간 내의 컴퓨터들을 연결하는 네트워크입니다. 알고스팟 대학교에서는 캠퍼스의 일&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;건물들의 위치가 2차원 좌표로 주어지고 현재 연결된 케이블들이 주어졌을때, 모든건물들을 연결하기 위한 최소 케이블 길이를 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 연결되어있는 간선들의 길이가 0이라 하고 kruscal알고리즘을 이용하여 문제를 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script src=&quot;https://gist.github.com/sbl133/9418b7cafc0c47cedef787d4cba42bd1.js&quot;&gt;&lt;/script&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 원본: &lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/LAN.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/LAN.cpp&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1649251326960&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - sbl133/JongmanBook&quot; data-og-description=&quot;Contribute to sbl133/JongmanBook development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/LAN.cpp&quot; data-og-url=&quot;https://github.com/sbl133/JongmanBook&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cnN8im/hyNXuYBEfo/3OiXBe7KcglftN8KQj0k00/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/LAN.cpp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/sbl133/JongmanBook/blob/main/31.%20MST/LAN.cpp&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cnN8im/hyNXuYBEfo/3OiXBe7KcglftN8KQj0k00/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - sbl133/JongmanBook&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to sbl133/JongmanBook development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;37&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brQ4yY/btryEsW0lfg/IkqqlGfc8gA2RqtAPODwW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brQ4yY/btryEsW0lfg/IkqqlGfc8gA2RqtAPODwW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brQ4yY/btryEsW0lfg/IkqqlGfc8gA2RqtAPODwW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrQ4yY%2FbtryEsW0lfg%2FIkqqlGfc8gA2RqtAPODwW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;37&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;37&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글을 통한 코드리뷰, 질문, 지적 언제든 환영입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reference: 프로그래밍 대회에서 배우는 알고리즘 문제해결전략2&lt;/p&gt;</description>
      <category>Algorithm/algospot</category>
      <author>buru_bum</author>
      <guid isPermaLink="true">https://sbl133.tistory.com/80</guid>
      <comments>https://sbl133.tistory.com/80#entry80comment</comments>
      <pubDate>Wed, 6 Apr 2022 22:23:09 +0900</pubDate>
    </item>
  </channel>
</rss>