ControllerAdvice를 사용할 수 있는 사례는 무엇이 있는가? 미션을 진행하는 과정에서 어디에 사용했는가?

kdkdhoho·2023년 4월 20일
0

Spring

목록 보기
21/26

@ControllerAdvice란? 🤔

API 동작 중에 예외가 발생했을 때, 예외를 바로 사용자에게 반환하는 것은 사용자 입장에서 불필요한 정보입니다. 따라서 이를 적절히 처리해 줄 필요가 있습니다.
이때 스프링의 @ControllerAdvice를 이용하면 API 동작 중에 발생한 예외를 적절히 처리할 수 있습니다.

@ControllerAdvice는 애플리케이션 내 모든 컨트롤러에서 발생하는 예외를 처리할 수 있습니다.
쉽게 생각해서 @RequestMapping이 달린 메서드 실행 중 발생한 예외를 가로채는 exceptions interceptor라고 생각하시면 됩니다.

참고: 여기서 Advice란, AOP와 관련된 용어입니다.
따라서 @ControllerAdvice는 AOP 기술이 적용되어 있음을 알 수 있습니다.

근데 @ControllerAdvice를 왜 쓰나요? 🤔

🔨 결론

결론부터 말씀드리자면 @ControllerAdvice를 사용하여 컨트롤러는 좀 더 컨트롤러의 역할에 집중할 수 있고, 코드의 중복도 제거할 수 있고, 관심사의 분리를 이뤄낼 수 있습니다.

🗒️ 설명

제가 구현한 웹 자동차 경주에서 발생할 수 있는 예외는 다음과 같습니다.

  1. 도메인 로직 내 검증으로 인한 IllegalArgumentException
  2. @Valid에 의한 유효성 검증으로 인한 MethodArgumentNotValidException

이 예외들을 실제 구현한 코드를 기반으로 각각의 메서드에서 처리해보겠습니다.

@RestController
@RequestMapping(value = "/plays")
public class WebController {

    // ...

    @PostMapping
    @ExceptionHandler({MethodArgumentNotValidException.class, IllegalArgumentException.class})
    public ResponseEntity<?> plays(@RequestBody @Valid final PlayRequest playRequest) {
        PlayResponse playResponse;

        try {
            playResponse = gameService.playRacing(playRequest.getNames(), playRequest.getCount());
        } catch (final Exception e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
        return ResponseEntity.ok().body(playResponse);
    }

    @GetMapping
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<?> plays() {
        List<PlayResponse> allGameHistory;

        try {
            allGameHistory = gameService.showGameHistory();
        } catch (final Exception e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
        return ResponseEntity.ok().body(allGameHistory);
    }
}

코드가 굉장히 지저분해지고, 제네릭 타입을 와일드 카드로 명시하게 되어 예상치 못한 결과를 반환할 확률이 매우 증가합니다.
또한 지금은 메서드가 2개이지만 서비스 규모가 커진다면 각각의 메서드에서 예외 처리하는 것은 분명 한계가 있습니다.

그러면 이제 @ControllerAdvice를 사용하여 개선해보겠습니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> bindExceptionHandler(final MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        FieldError fieldError = bindingResult.getFieldError();

        return ResponseEntity.badRequest().body(fieldError.getDefaultMessage());
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> runtimeExceptionHandler(final IllegalArgumentException. e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

@ControllerAdvice를 선언한 하나의 클래스(GlobalExceptionHanlder)를 통해 애플리케이션 내 모든 컨트롤러에서 발생할 수 있는 예외들을 처리할 수 있습니다.
또한 각 메서드별로 처리하는 예외를 다르게 할 수 있습니다.

이제 달라진 컨트롤러 코드를 보겠습니다.

@RestController
@RequestMapping(value = "/plays")
public class WebController {

    // ...

    @PostMapping
    public ResponseEntity<PlayResponse> plays(@RequestBody @Valid final PlayRequest playRequest) {
        PlayResponse playResponse = gameService.playRacing(playRequest.getNames(), playRequest.getCount());
        return ResponseEntity.ok().body(playResponse);
    }

    @GetMapping
    public ResponseEntity<List<PlayResponse>> plays() {
        List<PlayResponse> allGameHistory = gameService.showGameHistory();
        return ResponseEntity.ok().body(allGameHistory);
    }
}

코드가 매우 간결해졌습니다. 컨트롤러의 각 메서드가 무엇을 하는지 명확하게 파악할 수 있습니다.
또, 컨트롤러의 역할인 클라이언트의 요청과 그에 따른 응답을 하는 행위가 명확해졌습니다.

그러면 아무 @Controller 클래스에서 @ExceptionHanlder로 다른 컨트롤러에서 발생하는 예외들을 처리해줄 수는 없나요?

안됩니다!
왜냐하면, 컨트롤러 클래스에 @ExceptionHanlder를 해당 컨트롤러에서 발생한 예외만을 처리하기 때문입니다.

하지만 중요한 사실이 하나 있습니다.

만약, WebController 내에 @ExceptionHandler(Runtime.class)가 달린 메서드가 추가된다면 과연 우선 순위는 어떻게 될까요?
컨트롤러 내의 메서드가 호출될까요? 아님 @ControllerAdvice 내의 메서드가 호출될까요?

바로 컨트롤러 내의 메서드가 호출됩니다.

즉, 다음과 같은 순서로 예외를 처리할 메서드를 찾습니다.
1. 같은 컨트롤러에 위치한 @ExceptionHandler 메서드 중 해당 예외를 처리할 수 있는 메서드
2. @ControllerAdvice 클래스에 위치한 @ExceptionHandler 메서드 검색

properties

@ControllerAdvice에는 5가지의 속성이 있습니다.

그 중 3가지를 살펴보겠습니다.

  • String[] value, basePackages: 예외를 다룰 컨트롤러가 속한 패키지를 설정한다.
  • Class<? extends Annotation>[] annotations: 특정 애노테이션이 적용된 컨트롤러를 대상한다.
  • Class<?>[] assignableTypes: 특정 타입 또는 그 하위 타입인 컨트롤러를 대상한다.

Reference

profile
newBlog == https://kdkdhoho.github.io

1개의 댓글

comment-user-thumbnail
2023년 4월 23일

오.. 예외를 처리하는 메서드를 찾을 때 우선순위가 있군요 ㅎㅎ
처음 알았습니다 !!
잘 보고 갑니다 동호님 ^^

답글 달기