Filter Exception Handler

MoonJaeGyeong·2024년 10월 4일

개요


내가 인지한 문제 상황: 토큰쪽에서 발생되는 에러를 저희가 처리한 형식대로 Response를 출력하지 않길래 왜 ExceptionHandler 에서 잡아주는 에러로 던졌는데 왜 안잡아주지? 라는 생각을 하게 되었다.

생각해보니 당연한건데 그냥 GlobalExceptionHandler 라는 말에 너무 다 잡아줄 거라 생각했나 보다.


안잡히는 이유


나는 이 이유를 ExceptionHandler 에 코드를 보고 바로 알게 되었다

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    protected ApiResponse<Object> bindException(BindException e) {
        return ApiResponse.of(
            HttpStatus.BAD_REQUEST,
            e.getBindingResult().getAllErrors().get(0).getDefaultMessage(),
            null
        );
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    protected ApiResponse<Object> illegalArgumentException(IllegalArgumentException e) {
        return ApiResponse.of(
            HttpStatus.BAD_REQUEST,
            e.getMessage(),
            null
        );
    }

}

이게 프로젝트에서 에러를 처리하는 핸들러인데 어노테이션을 살펴보면 @RestControllerAdvice 라는 처리가 되어있는 것을 볼 수 있다. 말 그대로 Controller 단에서만 아래 에러들을 잡아준다는 것인데 우리가 던진 위의 에러는 Filter 단에서 던진 것이다.


스프링의 처리 과정


스프링의 Client 가 요청한 사항이 거쳐가는 과정을 나타낸 그림이다. 우선 크게 보자면 Filter 를 먼저 거치고 Dispatcher Servlet 을 거쳐서 Controller 로 가게 되는데 이 때 Controller 단에서 던진 에러만을 우리가 원하는 대로 에러처리를 할 수 있다고 한다.

결론적으로 말하자면 Filter 에서 던진 에러기 때문에 우리가 제작한 Filter 에서는 해당 에러를 우리가 원하는대로 던질 수 없다는 것이다.


해결 방법


1안. Controller로 예외 넘기기

그냥 단순하게 Controller 에서만 발생한 예외를 잡아준다면 Controller에서 예외가 발생하게 하면 되지 않을까? 생각했었는데

에러 처리 흐름을 보자면 만약 Controller 에서 예외가 발생한다면 예외 전달 흐름은

WAS (여기까지 전파) <-- Filter <-- Servlet <-- Interceptor <-- Controller (예외발생)

이런 식으로 진행되는데 만약 Filter 에서 발생한 예외를 Controller 로 넘긴다면

WAS (여기까지 전파) <-- Filter <-- Servlet <-- Interceptor <-- Controller (예외 받음) <-- Interceptor <-- Servlet <-- Filter(예외 발생)

이렇게 엄청난 비효율을 보여줄 수 있다. 흐름도 이상하고 효율적이지도 못한 거 같아 바로 패스했다.


2안. Jwt Filter 에서 Response에 값 넣기

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
        throws ServletException, IOException {
    try {
        String cookie_refreshToken = getRefreshTokenByRequest(request);
        String accessToken = jwtTokenUtil.getHeaderToken(request, ACCESS_TOKEN);
        String refreshToken = jwtTokenUtil.getHeaderToken(request, REFRESH_TOKEN);

        if (cookie_refreshToken != null) {
            processSecurity(accessToken, cookie_refreshToken, response);
        } else {
            processSecurity(accessToken, refreshToken, response);
        }

        filterChain.doFilter(request, response);
    } catch (JwtValidationException e) {
        response.setStatus(status.value());
        response.setContentType("application/json");
        ApiResponse apiResponse =  ApiResponse.of(
                HttpStatus.UNAUTHORIZED,
                ex.getMessage(),
                null
        );
        try{
            String jsonResponse = objectMapper.writeValueAsString(apiResponse);
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(jsonResponse);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

위와 같은 방식으로 처리 할까 했는데 Jwt Filter 에서 에러 처리도 잡아서 한다는게 이게 좋은 방식이 맞나? 너무 많은 책임을 가지고 있는 거 같아 해당 방식으로 잘 처리되긴 했지만 이를 분리해주는게 좋지 않을까 라는 생각이 들었다.


3안. 새로운 CustomFilter 작성

@Slf4j
@Component
public class ExceptionHandlerFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try{
            filterChain.doFilter(request, response);
        } catch (JwtException e){
            log.error(e.getMessage());
            setErrorResponse(HttpStatus.UNAUTHORIZED, response, e);
        }
    }

    public void setErrorResponse(HttpStatus status, HttpServletResponse response, Throwable ex){
        ObjectMapper objectMapper = new ObjectMapper();
        response.setStatus(status.value());
        response.setContentType("application/json");
        ApiResponse apiResponse =  ApiResponse.of(
                HttpStatus.UNAUTHORIZED,
                ex.getMessage(),
                null
        );
        try{
            String jsonResponse = objectMapper.writeValueAsString(apiResponse);
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(jsonResponse);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenFilter jwtTokenFilter;
    private final ExceptionHandlerFilter exceptionHandlerFilter;
    private final CustomOAuth2UserService customOAuth2UserService;


    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            .httpBasic(AbstractHttpConfigurer::disable)
            .csrf(AbstractHttpConfigurer::disable)
                .formLogin(FormLoginConfigurer::disable)
            .sessionManagement((sessionManagement) -> sessionManagement
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                    .requestMatchers("/**").permitAll()
            )
                .oauth2Login(customConfigurer -> customConfigurer
                        .userInfoEndpoint(endpointConfig -> endpointConfig.userService(customOAuth2UserService)))
            .headers((headerConfig)->
                headerConfig.frameOptions((HeadersConfigurer.FrameOptionsConfig::sameOrigin)
                )
            )
            .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(exceptionHandlerFilter, JwtTokenFilter.class);

        return httpSecurity.build();
    }
}

이런 식으로 Filter에서 발생한 예외를 처리해주는 Filter 를 따로 제작해서 SecurityConfig 에 등록해주었다. 이렇게 사용하니 Filter 에서 발생한 에외도 우리가 원하는 대로 Response 값을 주는 것을 볼 수 있다.

결론


뭐가 제일 좋은 방식인지는 아직도 잘 모르겠다. 그래도 이렇게 ExceptionFilterHandler 를 추가해주는게 내가 보기에는 가장 좋은 방식인 거 같아 그렇게 진행하였다. 이외에 방법이나 해당 주제에 대해 틀린게 있다면 알려주시면 감사하겠습니다.

profile
내 맘대로 끄적이는 개발 블로그

0개의 댓글