[혼자왔니] spring에서의 예외처리와 security filter

Update:     Updated:

카테고리:

태그:

예외처리 하기

프로그램에서 발생할 수 있는 여러 예외적인 상황에 대해 처리하는 것은 중요하다.

spring에서는 다음과 같은 방법들로 예외를 처리한다.

  • @ExceptionHandler
    • @Contrller, @RestController가 적용된 Bean내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리 해주는 기능을 한다.
      @RestController
      public class MyController {
        
          @GetMapping("/nullEx")
          @ResponseBody
          public String myController() {
              throw new NullPointerException();   // 이 경우에 한해서만 예외처리가 가능함
          }
        
          @GetMapping("/indexEx")
          @ResponseBody
          public String myController2() {
              throw new IndexOutOfBoundsException();
          }
        
          @ExceptionHandler(NullPointerException.class)  // NullPointerException만
          public Object nullEx(Exception e) {
              System.out.println(e.getClass());
              return "myServiceException";
          }
      }
    
  • @ControllerAdvice
    • 모든 @Cotroller에 대한 예외를 잡아서 처리해주는 기능
    • @RestControllerAdvice → @ControllerAdvice, @ResponseBody
      • 에러응답으로 객체 리턴(json) → @RestControllerAdvice
      • 에러응답으로 예외 페이지(viewResolver) → @ControllerAdvice
      /**
       * 1. custom exception을 만들어줌
       */
      public class UserNotFoundCException extends RuntimeException{
          public UserNotFoundCException() {
              super();
          }
        
          public UserNotFoundCException(String message, Throwable cause) {
              super(message, cause);
          }
        
          public UserNotFoundCException(String message) {
              super(message);
          }
      }
    
      /**
       * 2. @RestControllerAdvice가 등록되어있는 ExceptionAdvice에 해당 에러를 처리하기 위한 Handler를 만들어준다.
       */
      @RequiredArgsConstructor
      @RestControllerAdvice  // 모든 Controller에 대한 예외 통합 관리
      public class ExceptionAdvice {
          private final ResponseService responseService;
          private final MessageSource messageSource;
        
          private String getMessage(String code) {
              return getMessage(code, null);
          }
        
          private String getMessage(String code, Object[] args) {
              return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
          }
      /////////////////////////////////////////////////////////////////////////////////////////////////
          @ExceptionHandler(Exception.class)  // 일반 예외
          @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)   
          protected CommonResult defaultException(HttpServletRequest request, Exception e) {
              return responseService.getFailResult
                      (Integer.parseInt(getMessage("unKnown.code")), getMessage("unKnown.msg"));
          }
        
          /***
           * 유저를 찾지 못했을 때 발생시키는 예외
           */
          @ExceptionHandler(UserNotFoundCException.class)
          @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
          protected CommonResult userNotFoundException(HttpServletRequest request, Exception e) {
              return responseService.getFailResult
                      (Integer.parseInt(getMessage("userNotFound.code")), getMessage("userNotFound.msg"));
          }
      }
    
      /**
       * 3. 예외가 발생해야 되는 상황에서 발생시키기
       */
        
      /**
       * 회원 조회(id)
       * @param id
       * @return UserResponseDto
       */
      @Transactional(readOnly = true)
      public UserResponseDto findById(Long id) {
          User user = userJpaRepo.findById(id).orElseThrow(UserNotFoundCException::new);
          return new UserResponseDto(user);
      }
    

예외 메시지 관리하기

통합 예외 관리에서 중요한 것은 예외 메시지 관리이다.

enum으로 관리하는 방법 등등이 있지만, yaml에 정리해두고, 해당 yaml 파일을 참조해서 예외 메시지를 가져오는 방법도 있다.

  • MessageSource

      # application.yml
      spring:
        messages:
          basename: i18n/exception
          encoding: UTF-8
    
      @Bean
      public MessageSource messageSource(
                         @Value("${spring.messages.basename}") String basename,
                         @Value("${spring.messages.encoding}") String encoding) {
            YamlMessageSource ms = new YamlMessageSource();
            ms.setBasename(basename);
            ms.setDefaultEncoding(encoding);
            ms.setAlwaysUseMessageFormat(true);
            ms.setUseCodeAsDefaultMessage(true);
            ms.setFallbackToSystemLocale(true);
            return ms;
      }
    
      # exception_ko.yml
      unKnown:
        code: "-9999"
        msg: "알수 없는 오류가 발생하였습니다."
      userNotFound:
        code: "-1000"
        msg: "존재하지 않는 회원입니다."
    

문제 발생..?

spring security를 활용하여 jwt 기반 회원 인증/인가 시스템을 구현했다!

그럼 jwt 테스트를 진행해보자!

  • 테스트의 내용
    • jwt 없이 api를 요청한 경우
    • 형식이 맞지 않거나 만료된 token을 사용한 경우
    • 정상 token이지만 권한이 없는 경우

자, 그럼 custom 예외를 열심히 만들고, 해당 예외처리가 되는지 확인해보자..

  • 응답 내용(엥..?)

      {
      	"timestamp": "2023-xx-xxxx",
      	"status": 403,
      	"error": "Forbidden",
      	"path": "/api/user/id/1"
      }
    

왜 열심히 만든 Custom 예외가 안 터질까?

구글링 해본 결과.. spring security의 filter링과 관련이 있었다.

현재 만든 예외처리는 @RestControllerAdvice를 통해 처리하게 했는데, 이 말은 예외가 Spring이 처리가능한 영역까지 도달한 경우 처리하도록 했다는 의미

하지만 spring security는 servlet dispatcher 앞단에 위치합니다…

Spring Security 작동 구조

image

스프링 시큐리티 사용시 스프링은 DispatcherServlet 앞단에 Filter를 배치시켜서 요청을 가로챈다.

클라이언트에 접근 권한이 없다면 인증화면으로 자동 리다이렉트 시킨다.

그럼 어떻게 하죠?

앞서 말한 ‘테스트 내용’ 에 대해 각각 spring security가 제공하는 몇몇 애들을 상속 받아서 재정의하면 됩니다.

  1. 정상적으로 Jwt이 제대로 오지 않은 경우 - AuthenticationEntryPoint
  2. 정상적인 Jwt이 왔지만 권한이 다른 경우 - AccessDeniedHandler
  3. @PreAuthorize, @Secured 등의 애노테이션으로 리소스 접근 권한 설정하기

RUAlone 카테고리 내 다른 글 보러가기

댓글 남기기