본문 바로가기
PlayGround/마실가실 리팩토링

[1년 후 마실가실] ExceptionHandlerFilter

by HJ0216 2024. 11. 17.

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

 

 

Filter에서 Exception 처리가 되지 않아 길을 잃은지 어느덧 한 달..

드디어 필터를 활용하여 Exception 처리까지 왔습니다..

 

의도치않은 리리팩토링으로 인해 기능 구현은 제자리지만, 포기하지 않으면 과정입니다..

파이팅..⭐

 

 

 

그간의 이야기를 잠깐 해보자면, 저에겐 @RestControllerAdvice를 선언한 GlobalExceptionHanlder가 있었습니다(지금도 있습니다^^). GlobalExceptionHanlder는 Controller로 들어온 Exception을 처리하는데, Filter에서 오류가 발생하면 Controller로 들어오지 않아 Exception 처리가 안되던 문제가 있었습니다..

 

그래서, 지금은 ⭐ Filter 전용 Exception Filter⭐를 만들고자 합니다.

 

ExceptionHandlerFilter.java
@Component
@Slf4j
public class ExceptionHandlerFilter extends OncePerRequestFilter {

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    try {
      filterChain.doFilter(request, response);
    } catch (BusinessException e) {
      setErrorResponse(response, e.getErrorCode());
    }
  }

  private void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) {
    response.setStatus(errorCode.getHttpStatus().value());

    ErrorResponse errorResponse = ErrorResponse.builder()
                                               .code(errorCode.name())
                                               .message(errorCode.getMessage())
                                               .build();

    try {
      ObjectMapper mapper = new ObjectMapper();
      mapper.enable(SerializationFeature.INDENT_OUTPUT);

      String jsonResponse = mapper.writeValueAsString(errorResponse);
      response.getWriter().write(jsonResponse);

    } catch (IOException e) {
      log.error("Error writing response: {}", e.getMessage(), e);
    }
  }
}

 

기존에 사용하던 Error Response와 동일한 형식으로 작성하였습니다.

JSON 형식으로 Error를 반환해주고 싶기에 ObjectMapper를 사용했습니다.

기본값으로 ObjectMapper는 데이터가 한 줄로 출력이되기에, 들여쓰기 옵션도 추가해주었습니다.

 

SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  // 생략

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    http.httpBasic(AbstractHttpConfigurer::disable);

    http.csrf(AbstractHttpConfigurer::disable);

    http.cors(httpSecurityCorsConfigurer ->
        corsConfigurationSource()
    );

    http.sessionManagement(sessionManagement ->
        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    );

    http.authorizeHttpRequests(auth ->
        auth
            .requestMatchers(PERMIT_ALL_URL).permitAll()
            .requestMatchers(NEED_ROLE_URL).hasRole("USER")
            .anyRequest().authenticated()
    );
    http.addFilterBefore(characterEncodingFilter(), CsrfFilter.class);
    http.addFilterBefore(new JWTFilter(jwtUtils), LoginFilter.class);

    LoginFilter loginFilter = new LoginFilter(
        authenticationManager(authenticationConfiguration)
        , jwtUtils
        , redisUtils);
    loginFilter.setFilterProcessesUrl("/api/v2/users/login");
    http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);

    http.addFilterBefore(new CustomLogoutFilter(jwtUtils, redisUtils), LogoutFilter.class);

    http.addFilterAfter(new ExceptionHandlerFilter(), SecurityContextPersistenceFilter.class);

    return http.build();
  }
}

Exception을 담당하는 필터를 새로 만들어줬기에 SecurityConfig에 추가를 해줍니다.

다른 모든 필터에서 발생한 예외를 한 곳에서 처리하고자 Spring Security 필터 체인의 마지막에 위치하는 SecurityContextPersistenceFilter의 뒤에 설정하였습니다.

 

LoginFilter.java
@Slf4j
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

  // 생략

  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request,
      HttpServletResponse response,
      AuthenticationException failed) {
    log.error("Login failed: ", failed.getMessage());

    throw new BusinessException(INVALID_CREDENTIALS);
  }
}

기존에는 response에 status만 setting을 했었는데, 지금은 BusinessException을 throw했습니다.

제 ErrorResponse는 CustomErrorCode라는 별도의 Enum을 활용해서 code와 메시지를 리턴해주는데, 동일한 형식을 가져가려고 CustomException을 발생시켰습니다.

그래서 Filter에서 BusinessException이 발생하면, 최종 단계의 필터인 ExceptionHandlerFilter에서 catch로 잡혀 Error 메시지를 리턴하게 됩니다.

 

Exception에 따른 response 처리와 비즈니스 로직이 분리되어 단일 책임을 향해가는 기분이 듭니다🥸.

뿌듯하네요 🥸 .

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://thalals.tistory.com/451

 

Spring Security Exception Handling - Filter 단 예외 처리하기

오늘은 Spring Security 를 적용했지만 JWT 가 만료되거나, 잘못된 토큰일 경우 401 코드 뿐만아니라 에러 메세지까지 핸들링 해줄 수 있도록 설정해 주고자 합니다. 1. Spring Security 와 @ControllerAdvice 먼

thalals.tistory.com