본문 바로가기
백엔드(Back End)/Spring

[TIL]20230706 - 스프링 예외 처리 @ExeptionHandler, @ControllerAdvice

by tjsdn9803 2023. 7. 6.

오늘은 과제 레벨 4를 진행하였다.

과제 레벨4의 요구사항은 기존의 필터로 이루어졌던 인증인가절차를 스프링 시큐리티로 교체하는것이었는데 기존에 이미 스프링 시큐리티를 사용하여 인증인가 절차를 구현하였기 떄문에 기존에 부족하다 느꼈던 코드들을 수정하기로 하였다.

기존 회원가입시 중복 아이디가 있거나 이메일이 있을시, 게시글을 조회하는데 해당id의 게시글이 없을 시 등의 상황에 예외를 던지도록 Service의 메소드들을 작성했었는데 그동안 따로 예외처리를 하지 않아 클라이언트 입장에서 무엇때문에 정상적인 응답이 오지 않는지 알 방법이 없었다. 그래서 이에 대한 예외처리를 추가해주었다.

private Post findPost(Long id) {
        return postRepository.findById(id).orElseThrow(()->
                new IllegalArgumentException("선택한 메모는 존재하지 않습니다."));
}

PostService에서 PostRepository에서 입력받은 id를 통해 게시글을 찾는 함수이다. 만약 해당 id의 게시글이 존재하지 않는다면 "선택한 메모는 존재하지 않습니다."라는 메시지와 함께 예외를 던진다.

게시글을 조회하는 getPost, 게시글을 수정하는 updatePost, 게시글을 삭제하는 deletePost 등의 메소드에서 findPost를 사용하여 먼저 DB로 부터 영속성컨텍스트에 엔티티를 가져오기때문에 거의 모든 메소드에서 예외처리를 해주어야할 필요가 있었다.

또한 이외에도 수정, 삭제시 JWT토큰을 검사하여 작성자와 다를 경우에도 예외를 던져 예외처리를 해주어야 했다.

@DeleteMapping("/post")
    public ResponseEntity deletePost(@RequestParam Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        User user = userDetails.getUser();
        try{
            postService.deletePost(id,user);
        }catch (Exception e){
            return new ResponseEntity(new Result(e.getMessage(), 400), HttpStatusCode.valueOf(400));
        }

        return new ResponseEntity(new Result("삭제에 성공하였습니다", 200), HttpStatusCode.valueOf(200));
}

그래서 삭제를 담당하는 Controller의 메소드에서 예외 처리를 해주었다.

하지만 이외의 메소드들은 성공시의 반환타입이 Dto여야하는데 실패시에는 메시지와 함께 상태코드를 보내야 했기 때문에 어떻게 처리해야할지 몰랐다.

그래서 방법을 찾던 중 @ExeptionHandler와 @ControllerAdvice를 알게되었다.


@ExeptionHandler

@ExeptionHandler는 컨트롤러내에 작성하며 해당 컨트롤러에서 발생하는 예외를 처리한다.

@RestController
public class LineController {
    @GetMapping("/lines")
    public ResponseEntity<List<LineResponse>> getLines() {
        // ...
    }
    
    @PostMapping("/lines")
    public ResponseEntity<LineResponse> createLine(@RequestBody LineRequest lineRequest) {
        // ...
    }
    
    @ExceptionHandler(LineException.class)
    public ResponseEntity<ErrorResponse> handleLineException(final LineException error) {
        // ...
    }
}

@ExceptionHandler의 옵션으로 다루고 싶은 예외 클래스형식을 값으로 넣어주고 발생 시 수행하고 싶은 로직을 작성하면된다.

하지만 @ExceptionHandler는 해당 컨트롤러 내에서 발생하는 예외만 처리하므로 컨트롤러가 많아진다면 같은 예외에 대한 중복코드가 될것이다. 그렇다면 코드의 양도 늘어날 뿐더러 여러 컨트롤러에 있는 @ExceptionHandler를 유지, 보수하기도 힘들어 질것이다.

@ControllerAdvice

@ControllerAdvice는 이러한 단점을 해결해준다.

@ControllerAdvice는 전역으로 예외처리를 하는 어노테이션이다. 

클래스에 @ControllerAdvice어노테이션이 붙어있으면 해당 클래스 내에서 @ExceptionHandler메소드를 정의하여 예외를 처리하는 로직을 담을 수 있다.

@ControllerAdvice를 사용함으로써 많은 컨트롤러에서 사용되는 중복 @ExceptionHandler메소드를 방지할 수 있고 하나의 메소드로 모든 컨트롤러에서 작동할 수 있기때문에 유지 보수성 또한 향상시킬 수 있다.

@RestControllerAdvice는 @ControllerAdvice와 @ResponseBody를 합친것인데 이는 @RestController와 @Controller의 차이와 같다.

@RestController는 @ResponseBody어노테이션을 가지고있기 때문에 응답으로 JSON을 줄수 있습니다.

유의해야할점은 @RestControllerAdvice은 @RestController에서의 예외만 처리하는것이 아니라 @Controller에서의 예외도 처리할수 있고 @ControllerAdvice 또한 마찬가지입니다.

@RestControllerAdvice와 @ControllerAdvice의 차이는 반환 타입의 차이만 있을뿐입니다.

즉, @ControllerAdvice는 AOP를 이용한 예외처리 방식으로 application 전역에서 발생하는 모든 서비스의 예외를 한 곳에서 관리할 수 있게 해준다.

또한 글 초반 서비스 로직 성공시와 실패시의 반환 타입이 다를 경우에도 성공시의 반환타입은 기존 컨트롤러의 메소의 반환타입으로 지정하고 실패시 반환타입은 @RestControllerAdvice의 메소드의 반환타입으로 지정함으로써 다른 반환타입을 사용할 수 있다.