• Exception (feat. @ControllerAdvice를 이용한 Exception 전역화)

    2022. 6. 6.

    by. 내이름은 킹햄찌

    들어가며

    에러와 예외는 개발을 하면서 절대 피할 수 없는 문제이다. 현업에서 문제가 발생했을때 당황하지 않고 빠르게 트러블 슈팅을 해내는 분들을 보면 개발자로서 신뢰가 폭발하고 많이 배우고 싶다는 생각이 들었던 경험이 다분했던지라 본인은 훌륭한 개발자라면 반드시 이를 잘 다루는 스킬을 갖추어야 한다고 생각한다. 훌륭한 개발자가 되기 위한 길을 떠나보자. (Java-Spring 기준으로 작성)

     

     

    Exception ? Error ?

     

        에러(Error)

    예외에 대해 알아보기 위해 에러와 예외의 차이점이 무엇인지에 대해 알아볼 필요가 있다

    Error란 시스템에서 발생하는 문제로 시스템 에러와 같이 JVM에서 주로 발생하며 개발자가 제어 할 수 없는 범위에서 발생하는 문제이다.

     

     

        예외(Exception)

    반면에 예외의 경우 개발자가 구현한 로직에서 고려하지 못한 경우에 발생할 수 있다. 그 말은 충분히 예상할 수 있고 개발자가 컨트롤 할 수 있는 범위인것이다.

    위는 자바의 예외 클래스를 나타낸 것이다. 문제가 발생했을때 에러와 예외로 나누어지게 되고 예외는 또 Checked Exception과 Unchecked Exception으로 나누어 지게 된다

    그렇다면 이 둘의 차이는 또 무엇인가?

     

     

     

    Checked Exception VS Unchecked Exception

    이 둘의 가장 큰 차이는 예상을 할 수 있느냐 없느냐의 차이로 볼 수 있다. 그렇기 때문에 컴파일러는 컴파일 단계에서 Checked Exception의 경우에는 예외를 필수적으로 처리하게 하고, 이 경우에는 @Transactional에서도 롤백하지 않는다.(옵션을 설정하지 않은 경우) try catch로 예외를 처리하거나 상위 메서드로 넘겨줘야 한다. 반면에 Unchecked Exception은 예상할 수 없기때문에 처리 하지 않아도 컴파일러가 warning을 주지 않고 컴파일을 한다. 그리고 런타임에 발생하여 예상할 수 없었기 때문에 @Transactional의 기본 롤백 대상이 된다.

      Checked Exception Unchecked Exception
    처리여부 필수 예외 처리 하지 않아도 됨
    트랜잭션 Rollback 여부 기본 설정되지 않음 기본 설정
    대표 Exception IOException,
    SQLException
    NullPointException,
    IllegalArgumentExceptionm
    검증 컴파일 단계 런타임 단계

    다음은 예외를 잘 처리하여 코드 흐름을 제어할 수 있는 방법들을 알아보자

     

     

     

     

    예외 처리방법

    스프링에서는 예외처리 방법을 크게 3가지로 나눌 수 있다

    1. 메서드단위 처리 Method Level - try/catch
    2. 컨트롤러단에서 처리 Controller Level - @ExceptionHandler
    3. 전역 처리 Global Level - @ControllerAdvice

    정확히는 DispatcherServlet에서 발생하는 예외를 HandlerExceptionResolver 가 처리하는 처리 방법들이다.

    메서드 단위에서 처리하는 try/catch에 대해서는 따로 설명하지 않고, 컨트롤러 단에서 처리 부터 살펴보도록 하겠다.

     

     

     

     

    @ExceptionHander

    Exception 클래스를 속성으로 받아 예외를 처리할 수 있으며 Controller내부에서 발생한 Exception을 처리하여 준다.

    아래 예제 코드를 보면 PostController 내부에 있는 NoSuchElementFoundException.class에서 Exception이 발생했을때 handleNoSuchElementFoundException가 해당 Exception을 처리하도록 한다.

    @RestController
    @RequiredArgsConstructor
    public class PostController{
    
      private final PostService postService;
      
      @GetMapping("/post/{postId}")
      public Respone getPost(@PathVariable("postId") Long postId){  
            return postService.findOne(postId);
       }
    
      @ExceptionHandler(NoSuchElementFoundException.class)
      public ResponseEntity<String> handleNoSuchElementFoundException(NoSuchElementFoundExceptione) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
      }
    }
    
    

    주의 해야할 점은 @ExceptionHandler에 등록된 예외 클래스와 파라미터로 받는 예외 클래스가 동일 해야한다. 값이 다를 경우 런타임 시점에 에러를 발생 시킨다. 그래서 뒤에서 보게 되겠지만 RuntimeException을 상속받는 클래스를 만들어 해당 클래스에서 모든 예외처리를 하게 만들것이다.

     

     

     

     

    @ControllerAdvice

    규모가 어느정도 큰 서버를 개발 했을때는 Controller가 많을 수 있고 @ExceptionHandler를 사용했을때 여러 컨트롤러에서 발생하는 예외의 코드가 중복될 가능성이 다분하다 이러한 문제를 해결하기위해 Spring3.2에서 지원하고 있으며, Json포멧으로 응답을 내리고 싶을 경우 spring 4.3 부터@RestControllerAdvice 을 사용 할 수 있다. @ExceptionHandler 와 조합하여 사용해서 모든 Controller에 대한 예외처리를 한 번에 할 수 있다.

    @RestControllerAdvice
    public class ControllerAdvice {
    
      @ExceptionHandler(NoSuchElementFoundException.class)
      public ResponseEntity<String> handleNoSuchElementFoundException(NoSuchElementFoundExceptione) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
      }
    }
    
    

    이와 같이 @RestControllerAdvice를 이용하여 전역적으로 예외 처리를 했을 경우 하나의 클래스에서 전역적으로 예외처리를 할 수 있고 직접 정의한 코드를 내려 줄 수도 있다는 장점이 있다.

     

     

     

     

    전역 예외 적용

    RuntimeException을 상속 받아 모든 예외처리를 수행하게 할 CustomException클래스를 만들었다.

    @Getter
    public class CustomException extends RuntimeException {
    
        private final Integer code;
    
        public CustomException(String message,Integer code) {
            super(message);
            this.code = code;
        }
    }
    

     

     

    그리고 NotFoundException 케이스들만 모은 에러들을 관리하기 위해 NotFoundException 클래스를 만든다.

    public class NotFoundException extends CustomException {
        public NotFoundException(String message, Integer code) {
            super(message, code);
        }
    }
    

     

     

    에러코드는 enum 타입으로 정의해서 한곳에서 관리 한다.

    @Getter
    public enum ExceptionBoard {
    
        NOT_FOUND_MEMBER(new NotFoundException("사용자를 찾을 수 없습니다.",404)),
        NOT_FOUND_POST(new NotFoundException("게시글을 찾을 수 없습니다.",404)),
        
    
        private final CustomException exception;
    
        ExceptionBoard(CustomException e){
            this.exception = e;
        }
    }
    

     

     

    Controller에서 예외가 발생하면 낚아챌 ControllerAdvice 클래스

    @RestControllerAdvice
    public class ControllerAdvice {
    
        //400
        @ExceptionHandler(BadRequestException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public ExceptionResponse BadRequestExceptionHandler(BadRequestException e) {
            return new ExceptionResponse(e.getMessage(), e.getCode());
        }
    
    		//404
        @ExceptionHandler(NotFoundException.class)
        @ResponseStatus(HttpStatus.NOT_FOUND)
        public ExceptionResponse handleNotFoundException(NotFoundException e) {
            return new ExceptionResponse(e.getMessage(), e.getCode());
        }
    
        //500
        @ExceptionHandler(Exception.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ExceptionResponse handleException(Exception e) {
            return new ExceptionResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
    }
    

     

     

     

    아래는 예외가 발생할 부분인데, 개인 프로젝트에 적용된 일부이다.

    public class PostService {
    
        private final PostRepository postRepository;
    		@Transactional
    		    public Post update(Long id,PostDto postDto){
    		        Post post = postRepository.findById(id).orElseThrow(ExceptionBoard.NOT_FOUND_POST::getException);
    		        post.setTitle(postDto.getTitle());
    		        post.setContent(postDto.getContent());
    		        return post;
    		    }
    }
    

     

    존재하지 않는 postid를 입력하여 확인한다.

     

     

     

     

    ref

    https://bloowhale.tistory.com/28?category=936900

    https://bcp0109.tistory.com/303

    https://mangkyu.tistory.com/204

    https://github.com/binghe819/TIL/blob/master/Spring/기타/스프링 예외처리 개념 및 전략.md

    https://cheese10yun.github.io/spring-guide-exception/

    https://tecoble.techcourse.co.kr/post/2020-08-17-custom-exception/

     

    댓글