1. 기존 예외 처리 방법
이때까지 예외 처리는 딱히 방법이라고 할 것도 없었다.. 그냥 Spring에서 제공하는 exception을 메시지를 담아서 throw 해줬다..
throw new IllegalArgumentException("잘못된 요청입니다.");
근데 점점 예외 처리는 많이 해줘야 하고 유지보수도 해야 하기에 예외처리를 따로 빼낼 필요가 있었다.
이번에 적용하게 된 예외 처리 방식은 controller 부분에서 throw 되는 exception을 @ExceptionHandler와 @RestControllerAdvice를 사용하여 핸들링하는 방식이다.
2. @ExceptionHandler
이 어노테이션은 특정 클래스의 메서드에서 예외처리를 하기 위한 어노테이션이다. spring 공식문서에서는 이 어노테이션을 다음과 같이 설명하고 있다.
Annotation for handling exceptions in specific handler classes and/or handler methods.
사용하는 방법은 아래와 같다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = { CustomException.class })
protected ResponseEntity<ResponseMessage> handleCustomException(CustomException e) {
log.error("handleCustomException throw CustomException : {}", e.getErrorCode());
return ResponseMessage.toResponseEntity(e.getErrorCode());
}
}
GlobalExceptionHandler라는 클래스는 ResponseEntityExceptionHandler라는 클래스를 상속받는다.
안에는 handleCustomException라는 메서드가 있다. 이 메서드는 CustomException 타입인 e를 매개변수로 받아서 e의 errorCode를 로그 찍고 ResponseMessage 클래스의 toResponseEntity라는 클래스 메서드를 매개변수 e의 errorCode와 함께 실행시켜 그 결과를 리턴한다.
여기서 메서드 위에 보면 @ExceptionHandler라는 어노테이션이 value를 CustomException.class로 설정되어 있다. 이 어노테이션은 컨트롤러 어노테이션이 붙어있는 Bean에서 발생하는 예외를 처리할 수 있다. 그리고 이 어노테이션은 현재 클래스(GlobalExceptionHandler)에서 발생하는 RuntimeException만 핸들링이 가능하다. 그리고, 우리의 코드 같은 경우 value를 CustomException.class로 설정해 줬기에 CustomException으로 호출되는 exception만 핸들링해 준다.
하지만 우리의 목적은 전체 코드에서 발생하는 예외를 다 잡아주는 것이다. 이럴 때 사용하는 것이 @ControllerAdvice이다. @ControllerAdvice는 모든 컨트롤러(@Controller, @RestController)에 @ControllerAdvice에 정의된 메서드를 매핑시켜 준다.
@RestControllerAdvice는 @ControllerAdvice와 @Responsebody를 합친 어노테이션이다. 즉, ControllerAdvice와 동일한 역할을 수행하면서 예외를 body에 담아 리턴할 수 있다.
그래서 이 클래스는 @RestControllerAdvice를 붙이고 있기 때문에 안의 handleCustomException라는 메서드를 모든 컨트롤러에 매핑시킨다. 그러면 controller에서 CustomException으로 예외가 생기면 이 메서드의 @ExceptionHandler가 예외를 잡아 처리해 준다. 처리해 주는 방식은 예외의 errorCode를 가져와서 로그를 찍고 ResponseMessage의 toResponseEntity 함수를 호출한다.
2. CustomException
여기서 CustomException은 내가 새로 만든 예외이다.
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
}
이 클래스는 RuntimeException을 상속받고 있고 필드에는 ErrorCode 타입의 변수 하나만 갖고 있다. 그리고 @AllArgsConstructor 어노테이션으로 이 필드로 생성자를 생성한다.
에러코드 클래스에는 우리가 처리하려는 많은 예외들이 정리되어 담겨 있다!(예외처리의 핵심 파일!!)
3. ErrorCode
@Getter
@AllArgsConstructor
public enum ErrorCode {
/* 400 BAD_REQUEST : 잘못된 요청 */
INVALID_TOKEN(BAD_REQUEST, "토큰이 유효하지 않습니다"),
DUPLICATE_USER(BAD_REQUEST, "중복된 사용자가 존재합니다"),
NOT_PROPER_PASSWORD(BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
NOT_AUTHOR(BAD_REQUEST, "작성자만 삭제/수정할 수 있습니다."),
WRONG_ADMIN_TOKEN(BAD_REQUEST, "관리자 암호가 틀려 등록이 불가능합니다."),
/* 404 NOT_FOUND : Resource 를 찾을 수 없음 */
USER_NOT_FOUND(NOT_FOUND, "등록된 사용자가 없습니다"),
POST_NOT_FOUND(NOT_FOUND, "선택한 게시물을 찾을 수 없습니다."),
COMMENT_NOT_FOUND(NOT_FOUND, "선택한 댓글을 찾을 수 없습니다."),
;
private final HttpStatus httpStatus;
private final String detail;
}
위의 코드는 내가 등록한 예외들이다. 우선 ErrorCode는 enum 타입이다. 안에는 예외들이 HttpStatus와 String을 가진채 열거되어 있다. 우리는 이렇게 새로 필요한 예외가 생길 때마다 밑에 하나씩 추가할 수 있다.
4. ResponseMessage
@Getter
@Builder
public class ResponseMessage {
private final LocalDateTime timestamp = LocalDateTime.now();
private final int status;
private final String error;
private final String code;
private final String message;
public static ResponseEntity<ResponseMessage> toResponseEntity(ErrorCode errorCode) {
return ResponseEntity
.status(errorCode.getHttpStatus())
.body(ResponseMessage.builder()
.status(errorCode.getHttpStatus().value())
.error(errorCode.getHttpStatus().name())
.code(errorCode.name())
.message(errorCode.getDetail())
.build()
);
}
}
ResponseMessage 클래스는 5개 속성을 가지고 있는데 각각 timestamp(현재시각), status, error, code, message이다.
나는 toResponseEntity라는 함수를 정의하여 enum 타입의 ErrorCode를 매개변수로 받아와 그 안에 내가 정의해 둔 httpStatus와 메시지를 이용해서 ResponseEntity를 반환해 주었다.
5. 실제 사용하는 코드
if (token == null || !validateToken(token)) {
throw new CustomException(ErrorCode.INVALID_TOKEN);
}
이 코드는 토큰이 없거나 잘못된 토큰이면 예외를 throw 하는 코드이다. 원래는 바로 spring 내장 exception인 IllegalArgumentException을 던져줬지만 지금은 내가 새로 정의한 CustomException을 던져준다.
CustomException을 throw 할 때 내가 정의한 Enum 타입의 에러코드도 같이 매개변수로 던져준다.
이 경우 토큰이 잘못된 거기 때문에 Enum 타입의 ErrorCode에 INVALID_TOKEN이라는 이름으로, httpStatus는 BAD_REQUEST로, detail은 "토큰이 유효하지 않습니다."로 만들었었다.
INVALID_TOKEN(BAD_REQUEST, "토큰이 유효하지 않습니다")
그래서 new CustomException(...)로 객체를 생성했기에 @AllArgsConstructor에 의해 생성자가 호출되어 매개변수로 넣어준 에러코드가 CustomException 클래스의 속성이 errorCode에 저장된다.
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
}
그러므로 이 CustomException이 발생했다는 것을 GlobalExceptionHandler 클래스에 정의해 뒀던 메서드가 캐치해서 받은 에러코드의 httpStatus로 로그를 찍고 ResponseMessage의 메서드를 호출하여 ResponseEntity를 반환한다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = { CustomException.class })
protected ResponseEntity<ResponseMessage> handleCustomException(CustomException e) {
log.error("handleCustomException throw CustomException : {}", e.getErrorCode());
return ResponseMessage.toResponseEntity(e.getErrorCode());
}
}
참고문헌:
https://ss-hoon.github.io/spring/exception_handling/
예외 처리(Exception Handling)
1. 들어가기
ss-hoon.github.io
https://developjuns.tistory.com/32
[SpringBoot] Exception Handler 예외를 통합관리 하자.!!!
요즘 YOPLE 서비스를 개발하면서 Exception을 핸들링해야 하는 일이 생겼다. 스프링 구조상 컨트롤러-서비스-JDBC lib를 통해 API가 처리되면서 Controller단에서 Throw 되는 Exception을 핸들링하고자 한다.
developjuns.tistory.com