

내가 인지한 문제 상황: 토큰쪽에서 발생되는 에러를 저희가 처리한 형식대로 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 에서는 해당 에러를 우리가 원하는대로 던질 수 없다는 것이다.
그냥 단순하게 Controller 에서만 발생한 예외를 잡아준다면 Controller에서 예외가 발생하게 하면 되지 않을까? 생각했었는데

에러 처리 흐름을 보자면 만약 Controller 에서 예외가 발생한다면 예외 전달 흐름은
WAS (여기까지 전파) <-- Filter <-- Servlet <-- Interceptor <-- Controller (예외발생)
이런 식으로 진행되는데 만약 Filter 에서 발생한 예외를 Controller 로 넘긴다면
WAS (여기까지 전파) <-- Filter <-- Servlet <-- Interceptor <-- Controller (예외 받음) <-- Interceptor <-- Servlet <-- Filter(예외 발생)
이렇게 엄청난 비효율을 보여줄 수 있다. 흐름도 이상하고 효율적이지도 못한 거 같아 바로 패스했다.
@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 에서 에러 처리도 잡아서 한다는게 이게 좋은 방식이 맞나? 너무 많은 책임을 가지고 있는 거 같아 해당 방식으로 잘 처리되긴 했지만 이를 분리해주는게 좋지 않을까 라는 생각이 들었다.
@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 를 추가해주는게 내가 보기에는 가장 좋은 방식인 거 같아 그렇게 진행하였다. 이외에 방법이나 해당 주제에 대해 틀린게 있다면 알려주시면 감사하겠습니다.