본문 바로가기

개발

[SpringBoot] Custom Exception

Q. Custom Exception을 만드는 이유?

Standard Exception만으로 커버하기 힘든 경우가 있기때문

  • 여러 예외 상황과 맞물려 비즈니스 로직의 명확성을 높인다.
  • 예외에 대한 응집도를 향상시키고싶은 경우 → Standard 사용시, 같은 예외를 발생시키는 장소가 많아지면 중복 예외 코드가 생기게 되는 문제가 발생한다. Custom을 사용하면 예외 발생 후 후처리가 용이해진다.
  • 상세한 예외 정보를 제공하고싶은 경우 
  • 예외 생성 비용을 줄일 수 있음 → trace를 재정의함으로써 stack trace 생성 비용을 줄일 수 있다.

 


@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException{
    private final ErrorCode errorCode;

    @Override
    public String getMessage() {
        return errorCode.getDetail();
    }
}

 

@Getter
@AllArgsConstructor
public enum ErrorCode {

    /* 400 BAD_REQUEST : 잘못된 요청 */
    BAD_RIOT_REQUEST(BAD_REQUEST, "데이터를 불러올 수 없습니다.");
    private final HttpStatus httpStatus;
    private final String detail;

}

해당 에러와 관련된 메세지에 추가로 HttpStatus도 전달할 수 있다.

 

@Getter
public class ErrorResponse {
    private final LocalDateTime timestamp = LocalDateTime.now();
    private final String error;
    private final String message;

    @Builder
    private ErrorResponse(String error, String message){
        this.error = error;
        this.message = message;
    }
    public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode){
        return ResponseEntity
                .status(errorCode.getHttpStatus())
                .body(ErrorResponse.builder()
                        .error(errorCode.getHttpStatus().name())
                        .message(errorCode.getDetail())
                        .build()
                );

    }
}

 

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ExceptionHandler(value = {ConstraintViolationException.class, MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class})
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(Exception e){
        log.error("handleMethodArgumentNotValidException throw CustomException : {}", e.getMessage());
        return ErrorResponse.toResponseEntity(ErrorCode.valueOf("INVALID_INPUT_VALUE"));
    }


    @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e){
        log.error("handleHttpRequestMethodNotSupportedException throw CustomException : {}", e.getMessage());
        return ErrorResponse.toResponseEntity(ErrorCode.valueOf("METHOD_NOT_ALLOWED"));

    }
    @ExceptionHandler(value = {CustomException.class})
    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e){
        log.error("handleCustomException throw CustomException : {}", e.getErrorCode());
        return ErrorResponse.toResponseEntity(e.getErrorCode());
    }
}

@ControllerAdivice는 @Controller 어노테이션이 적용된 모든 곳에서의 발생되는 예외에 대해 Catch 합니다.

 

[ ControllerAdvice의 동작 과정 ]

  1. 디스패처 서블릿이 에러를 catch함
  2. 해당 에러를 처리할 수 있는 처리기(HandlerExceptionResolver)가 에러를 처리함
  3. 컨트롤러의 ExceptionHandler로 처리가능한지 검사함 → Controller에 있는 ExceptionHandler가 우선 순위를 갖도록 먼저 컨트롤러의 ExceptionHandler를 검사한다.
  4. ControllerAdvice의 ExceptionHandler로 처리가능한지 검사함 컨트롤러에서 갖는 ExceptionHandler로 처리가 불가능하다면 등록된 모든 ControllerAdvice 빈을 검사한다.
  5. ControllerAdvice의 ExceptionHandler 메소드를 invoke하여 예외를 반환함 리플렉션 API를 이용해 ExceptionHandler의 구현 메소드를 호출해 처리한 에러를 반환한다.

 

Q. @ControllerAdvice는 AOP방식으로 구현되었는데, AOP방식이란? 

 핵심 비즈니스 기능과 구분하기 위해 공통 기능을 공통 관심 사항(cross-cutting concern)이라고 표현하며, 핵심 로직을 핵심 관심 사항(core concern)이라고 표현한다. 공통 관심 사항들을 객체 지향 기법(상속이나 위임 등)을 사용해서 여러 모듈에 효과적으로 적용하는 데는 한계가 있으며, 중복된 코드를 양산하곤 한다. 이런 한계를 극복하기 위해 AOP라는 기법을 사용한다.

 

Aspect Oriented Programming, 줄여서 AOP는 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법을 말한다. AOP는 문제를 해결하기 위한 핵심 관심 사항과 전체에 적용되는 공통 관심 사항을 기준으로 프로그래밍 함으로써 공통 모듈을 여러 코드에 쉽게 적용 할 수 있도록 도와준다.

 


예외 테스트

ErrorResponse에서 선언한 변수인 timestamp, error, message가 반환됨

 

2023-09-25T21:29:35.490+09:00 ERROR 3838 --- [nio-8080-exec-2] c.example.gg.error.ExceptionController   : handleCustomException throw CustomException : BAD_RIOT_REQUEST

ExceptionController에서 작성한 내용이 콘솔창에 출력