이전 포스팅에서 프로젝트 애플리케이션이 동작 시 발생하는 다양한 상황에 대해서 메세지를 클라이언트로 전달해주기 위해 Custom Exception을 이용한 사용자 예외 처리를 다뤘다.
이렇게 Custom Exception을 사용하다보니 Web Layer (Controller)에서 로직을 처리해 줄 때 발생하는 예외를 try-catch로 매 로직마다 처리해주어야 했다.
try-catch문에 들어가는 로직은 status, message를 담아 보내주는 공통적인 로직인데 이를 한 군데서 전역 처리할 수 없을까?
@RestControllerAdvice, @ControllerAdvice
ControllerAdvice, RestControllerAdvice는 모든 Controller에서 발생한 예외를 한 곳에저 전역 처리해주는 애노테이션이다.
@RestControllerAdvice와 @ControllerAdvice는 무슨 차이가 있을까 ?
일반적으로, 모델과 뷰를 통합해서 쓰는 @Controller의 경우 @ControllerAdvice를 사용하고 Controller를 API 서버로 사용하는 @RestController의경우는 @RestControllerAdvice로 사용한다.
실제로 애노테이션 문서를 확인해보면 RestControllerAdvice에는 ControllerAdvice와 ResponseBody가 함께 들어간 애노테이션임을 확인할 수 있다. (RestControllerAdvice = ControllerAdvice + ResponseBody)
@ExceptionHandler
@ExceptionHandler는 Spring 프레임워크에서 @Controller나 @RestController가 적용된 Bean에서 try-catch를 이용한 예외처리를 애노테이션을 통해 간편한 처리를 도와주는 애노테이션이다.
아래의 코드는 동일한 역할을 하는 코드이다.
try {
// logic
} catch (CustomException e) {
// exception logic
}
@ExceptionHandler(CustomException.class)
public ResponeEntity<?> handle(CustomException e) {
// exception logic
}
@RestControllerAdvice, @ExceptionHandler를 이용한 전역 예외 처리
@ControllerAdvice를 보면 @ExceptionHandler로 발생하는 예외의 가장 높은 우선순위의 루트 예외를 사용하라고 되어있다.
이전에는 상황별 예외 처리를 위해 여러 개의 Custom Exception 클래스를 RuntimeException을 상속받은 후 만들어 사용했다.
하지만 이전의 경우를 그대로 가져간다면, 예외를 한번에 전역처리할 수 있는 클래스가 RuntimeException이 되기 때문에 우리가 잡고자 하는 사용자 예외 뿐만 아니라 RuntimeException으로 잡히는 모든 예외들이 잡힐 것이다.
그렇다고 각 예외마다 하나씩 ExceptionHandler를 놓는 것은 사실상 전역 처리하는 의미가 없어진다.
CommonExcption을 만들자!
그렇다면 이 문제를 해결하려면 어떻게 해야할지 고민해봤다.
우리가 선택한 방법은 CustomException과 RuntimeException사이에 RuntimeException을 상속받는 공통 예외 클래스를 하나 놓고, CustomException은 공통 예외 클래스를 상속받아 사용하는 방식으로 해결했다.
public abstract class NbbangCommonException extends RuntimeException{
public NbbangCommonException(String message) {
super(message);
}
public abstract String getErrorCode(); // Enum 타입에 지정한 에러코드
public abstract HttpStatus getHttpStatus(); // HttpStatus Code
public abstract String getMessage(); // 실제 호출 메세지
}
위와 같이 CustomException과 RuntimeException 사이에 추상 클래스로된 CommonException을 만들었다.
이제 CustomException에서 CommonException을 상속받아 만들어 놓은 메소드를 구현하면, ControllerAdvice Docs에 정의된 요구대로 ExceptionHandler에서 CustomException보다 상위 예외를 호출 시킴으로써 예외에 대한 전역처리가 가능해질 것이다.
CustomException 수정
먼저 기존 CustomException을 확인해보자.
public class NoSuchOttException extends RuntimeException {
private final NbbangException nbbangException;
public NoSuchOttException(String message, NbbangException nbbangException) {
super(message);
this.nbbangException = nbbangException;
}
public NoSuchOttException(NbbangException nbbangException) {
super(nbbangException.getMessage());
this.nbbangException = nbbangException;
}
public NbbangException getNbbangException() {
return this.nbbangException;
}
}
CustomException을 CommonException을 상속받게 수정하면 아래와 같다.
public class NoSuchOttException extends NbbangCommonException {
private final String message;
private final NbbangException nbbangException;
public NoSuchOttException(String message, NbbangException nbbangException) {
super(message);
this.message = message;
this.nbbangException = nbbangException;
}
@Override
public String getErrorCode() {
return nbbangException.getCode();
}
@Override
public HttpStatus getHttpStatus() {
return HttpStatus.OK;
}
@Override
public String getMessage() {
return message;
}
}
@ RestControllerAdvice, @ExceptionHandler 전역 처리
CustomException과 CommonException을 구현했으므로 마지막으로 전역처리할 Handler를 만들어보자.
@RestControllerAdvice
@Slf4j
public class NbbangExceptionHandler {
@ExceptionHandler(NbbangCommonException.class)
public ResponseEntity<CommonResponse> handleCommonException(NbbangCommonException e) {
log.warn("Nbbang Exception Code : " + e.getErrorCode());
log.warn("Nbbang Exception message : " + e.getMessage());
return ResponseEntity.ok(CommonResponse.response(false, e.getMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<CommonResponse> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("IllegalArgument Exception");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(CommonResponse.response(false, "Bad Request"));
}
}
ExceptionHandler에 CommonException을 걸어주고 Exception 로직을 작성해준다.
프로젝트에서 CustomException이 발생하는 경우에는 OK와 함께 status = false, message = 예외 메세지 포맷으로 응답하기로 결정했기때문에 OK를 사용했다.
Controller 테스트를 통해 확인해보자 !
간단한 테스트 코드를 작성해서 제대로 예외가 발생하는지 확인해보자.
@Test
@DisplayName("Ott 컨트롤러 : Ott 서비스 조회 실패")
void ott_서비스_조회_실패() throws Exception {
// given
String uri = "/ott/1";
given(ottService.findOtt(anyLong())).willThrow(new NoSuchOttException("조회 실패", NbbangException.NOT_FOUND_OTT)); // 예외 테스트를 위한 예외 발생
// when
// ExceptionHandler로 잡힌 logic -> status : OK, Message : 예외 발생 시 작성한 메세지 출력 확인
MockHttpServletResponse response = mvc.perform(get(uri))
.andExpect(status().isOk())
.andDo(print())
.andExpect(jsonPath("$.status").value(false))
.andExpect(jsonPath("$.message").value("조회 실패"))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
}
MockMvc 객체를 이용해 호출 테스트를 진행했다.
서비스 로직에서 예외를 발생시키고, 예외 처리시 작성한 메세지가 ExceptionHandler의 Exception 로직을 제대로 타는지 확인해봤다.
ExceptionHandler에 작성한 Logic (log.warn 부분)이 제대로 호출되었다.
클라이언트로 전달되는 응답도 status : false와 message : 예외 처리 메세지로 잘 전달된 것을 확인할 수 있다.
📄 References
[Spring] @RestControllerAdvice : https://lollolzkk.tistory.com/39#:~:text=서버단에서 로직을,를 클라이언트에 반환해준다.&text=클래스 내부의 메서드들은,하는 로직이 들어있다.
@ControllerAdvice, @ExceptionHandler를 이용한 예외처리 분리, 통합하기(Spring에서 예외 관리하는 방법, 실무에서는 어떻게?) : https://jeong-pro.tistory.com/195
'Backend > Spring Boot' 카테고리의 다른 글
Converter를 이용해 URI에 Enum 타입 매핑하기 (0) | 2022.06.26 |
---|---|
Spring Cloud Config의 설정 파일 비대칭키로 암/복호화 (0) | 2022.06.03 |
Spring Cloud Gateway를 이용한 서비스 라우팅 및 JWT 토큰 검증 (2) | 2022.05.24 |
Custom Exception을 이용한 예외 처리 (0) | 2022.05.22 |
Request 스코프와 Proxy (0) | 2022.05.16 |