Spring의 기본적인 예외 처리 방법이 WAS까지 에러가 전달된다는 문제점과 코드의 가독성 등의 이유로 HandlerExceptionResolver를 구현하여 예외를 처리한다.
구현체들 중 ExceptionHandlerExceptionResolver, 그리고 이를 위해 @RestControllerAdvice를 사용하는 이유를 공부해 보았다.
컨트롤러에서 예외가 발생했을 때, 별도의 예외처리를 하지 않으면 WAS까지 에러가 전달됨
→ WAS는 에플리케이션에서 처리를 못하는 예외라 판단하고 컨트롤러를 한번 더 호출
Java의 예외 처리 방법: try-catch ⇒ 모든 코드에 붙이는 것은 비효율적
예외처리 전략 추상화 → 메인 로직으로부터 분리
예외를 catch하고 HTTP 상태, 응답 메세지 설정
WAS는 해당 요청을 정상적인 응답으로 인식하여 에러 전달X
HandlerExceptionResolver
구현체들을 빈으로 등록해서 관리한다.
DefaultErrorAttributes
: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.ExceptionHandlerExceptionResolver
: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함ResponseStatusExceptionResolver
: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함DefaultHandlerExceptionResolver
: Spring 내부의 기본 예외들을 처리아래의 도구들로 ExceptionResolver를 동작시켜 에러를 처리한다.
@ResponseStatus
@ResponseStatusException
@ExceptionHandler
@ControllerAdvice, RestControllerAdvice
@ResponseStatus
: 에러 HTTP 상태를 변경하도록 도와주는 어노테이션
Exception 클래스 자체, 메소드에 @ExceptionHandler와 함께, 클래스에 @RestControllerAdvice와 함께 적용 가능
@ResponseStatusException
: @ResponseStatus
와 마찬가지로ResponseStatusExceptionResolver
가 에러를 처리
기본적인 예외 처리를 빠르게 적용할 수 있어 손쉽게 프로토타이핑 할 수 있고, HttpsStatus를 직접 설정할 수 있다는 장점이 있음
컨트롤러의 메소드, @ControllerAdvice
나 @RestControllerAdvice
가 있는 클래스의 메소드에 @ExceptionHandler
어노테이션을 추가하여 에러를 처리한다.
발생한 예외는 ExceptionHandlerExceptionResolver
에 의해 처리
Exception 클래스들을 속성으로 받아 처리할 예외를 지정
예외 클래스를 지정하지 않는다면 파라미터에 설정된 에러 클래스를 처리
에러 응답을 자유롭게 다룰 수 있음
@ExceptionHandler(NoSuchElementFoundException.class)
public ResponseEntity<ErrorResponse> handleItemNotFoundException(NoSuchElementFoundException exception) {
...
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
...
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception exception) {
...
}
Spring은 예외 발생 시 가장 구체적인 예외 핸들러를 찾고, 없으면 부모 예외의 핸들러를 찾음
이때 @ExceptionHandler에 등록된 예외 클래스와 파라미터로 받는 예외 클래스가 동일 해야 함
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception exception) {
...
}
여러 컨트롤러에 대해 전역적으로 @ExceptionHandler
를 적용
응답을 Json으로 내려주는 차이점
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity<Object> onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
}
장점:
주의점:
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity<Object> onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
...
}
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
...
// 멤버 관련 응답
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
...
// For test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "테스트용입니다.");
...
}
@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService {
@Override
public void CheckFlag(Integer flag) {
if (flag == 1) {
throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
}
}
}
public class TempHandler extends GeneralException {
public TempHandler(BaseErrorCode errorCode) {
super(errorCode);
}
}
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus() {
return this.code.getReasonHttpStatus();
}
}
서비스에서 예외 발생이 확인되면 TempHandler
를 통해 GeneralException
을 생성하고, 이는 RuntimeException
을 상속 받았기 예외로 던져진다.
그럼 @RestControllerAdvice
가 있는 ExceptionAdvice
에서 @ExceptionHandler
인 적절한 메소드를 찾아 예외를 처리하게 되는 것이다.
참고 자료
https://mangkyu.tistory.com/204#recentEntries
[MangKyu's Diary:티스토리]