모르지 않다는 것은 아는것과 다르다.

Spring

@ControllerAdvice

채마스 2022. 3. 19. 20:59

ControllerAdvice 란?

  • @ExceptionHandler 를 모아서 글로벌하게 적용할 때 쓰는 애노테이션
  • 종류
    • @ControllerAdvice
    • @RestControllerAdvice = @ControllerAdvice + @ResponseBody
  • 속성
    • value == basePackages
    • basePackages: 적용 범위를 문자열을 이용해 특정 패키지로 지정
    • basePackageClasses: 적용 범위를 대표 클래스 한 개를 이용해 특정 패키지로 지정
    • basePackages 를 type-safe 하게 사용하기 위해 제공하는 옵션
      • assignableTypes: 적용 범위를 특정 클래스에 할당할 수 있는 컨트롤러로 지정
    • annotations: 적용 범위를 특정 애노테이션을 사용한 컨트롤러로 지정

 

코드 예시

  • ErrorCode, GeneralException, APIErrorResponse 을 구현한 부분은 생략한다.
@RestControllerAdvice(annotations = RestController.class)
public class ApiExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<APIErrorResponse> general(GeneralException e) {
        ErrorCode errorCode = e.getErrorCode();
        HttpStatus status = errorCode.isClientSideError() ?
                HttpStatus.BAD_REQUEST :
                HttpStatus.INTERNAL_SERVER_ERROR;

        return ResponseEntity
                .status(status)
                .body(APIErrorResponse.of(
                    false, errorCode, errorCode.getMessage(e)
                ));
        );
    }

    @ExceptionHandler
    public ResponseEntity<APIErrorResponse> exception(Exception e {
        ErrorCode errorCode = ErrorCode.INTERNAL_ERROR;
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;

        return ResponseEntity
                .status(status)
                .body(APIErrorResponse.of(
                    false, errorCode, errorCode.getMessage(e)
                ));
    }
}
  • 먼저 API 의 에러만을 핸들링 하기위해서 @RestControllerAdvice(annotations = RestController.class) 로 설정했다. -> 화면을 반환하는 경우는 제외된다.
  • 직접 커스텀한 GeneralException 의 경우, general 에서 핸들링된다.
    • 클라이언트 쪽 에러인지, 서버쪽 에러인지 체크해서 상태를 반환한다.
    • 바디에는 에러 메세지를 넣어준다.
  • 다음으로, 그 밖에 에러들은 exception 에서 핸들링된다.
    • 서버에러로 간주하고 상태를 반환한다.
    • 바디에는 에러 메세지를 넣어준다.
  • 하지만 저렇게만 구현하면 Spring MVC 에서 내부적으로 발생하는 예외들을 상세하게 처리할 수 없다.
    • ResponseEntityExceptionHandler 를 구현하면, 위의 문제를 해결할 수 있다.

 

ResponseEntityExceptionHandler

  • Spring MVC 에서 내부적으로 발생하는 예외들을 처리하는 클래스다.

  • 위와 같이 다양한 에러를 핸들링 해준다.
  • API 예외 처리를 담당하는 @ControllerAdvice 클래스에서 상속 받아 사용한다.
  • 커스터마이징을 원하는 특정 메소드를 오버라이드해서 커스텀 할 수 있다.
    • 아래의 handlerExceptionInternal 메소드를 오버라이드 해서 body 에 값을 넣어줄 것이다.

  • 위와 같이 현재는 body 가 null 로 세팅되어져 있다.
@RestControllerAdvice(annotations = RestController.class)
public class APIExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<Object> general(GeneralException e, WebRequest request) {
        ErrorCode errorCode = e.getErrorCode();
        HttpStatus status = errorCode.isClientSideError() ?
                HttpStatus.BAD_REQUEST :
                HttpStatus.INTERNAL_SERVER_ERROR;

        return super.handleExceptionInternal(
                e,
                APIErrorResponse.of(false, errorCode.getCode(), errorCode.getMessage(e)),
                HttpHeaders.EMPTY,
                status,
                request
        );
    }

    @ExceptionHandler
    public ResponseEntity<Object> exception(Exception e, WebRequest request) {
        ErrorCode errorCode = ErrorCode.INTERNAL_ERROR;
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;

        return super.handleExceptionInternal(
                e,
                APIErrorResponse.of(false, errorCode.getCode(), errorCode.getMessage(e)),
                HttpHeaders.EMPTY,
                status,
                request
        );
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorCode errorCode = status.is4xxClientError() ?
                ErrorCode.SPRING_BAD_REQUEST :
                ErrorCode.SPRING_INTERNAL_ERROR;

        return super.handleExceptionInternal(
                ex,
                APIErrorResponse.of(false, errorCode.getCode(), errorCode.getMessage(ex)),
                headers,
                status,
                request
        );
    }
}
  • APIErrorResponse.of(false, errorCode.getCode(), errorCode.getMessage(ex)) 로 바디를 세팅해 줬다.
  • 보는 것과 같이 반환값을 부모의 handleExceptionInternal 다시 호출한다. -> body 를 채워주는것 말고는 부모의 로직을 그대로 따르게 구현하기 위함이다.
  • general, exception 도 handleExceptionInternal 메소드로 반환함으로써, 통일성을 맞출 수 있다.
    • 하지만 반환값이 ResponseEntity 가 아닌 ResponseEntity 로 변경해야 한다.

 

REFERENCES

  • 김은호님의 SpringMVC

'Spring' 카테고리의 다른 글

함수 기반 API 설계  (0) 2022.03.19
Spring Cache Abstraction  (0) 2022.03.19
설정파일(Properties) 관리  (0) 2022.03.19
Null Safety  (0) 2022.03.14
프락시 팩토리빈과 @AspectJ  (0) 2022.03.11