서버에서 클라이언트의 요청을 처리하던 도중 예외가 발생한다면 서버는 디폴트로 HTTP 500 Interval Server Error
상태의 응답을 전송한다. 하지만 서버 프로그램에서 발생하는 예외를 처리해줌으로써 예외에 따라 클라이언트에게 500
응답 이외의 개발자가 정해둔 다른 상태 코드
와 오류 메시지
를 내려줄 수 있다.
프로그램에서 예외 처리 구문들을 이용해서 예외를 처리해주면 예외를 맞닥드리더라도 프로그램이 죽지 않고 동작하지만 클라이언트에게 올바른 결과를 리턴하지 못하고 단순히 500
응답을 리턴한다. Spring
에서는 예외가 발생했을때 예외를 감지해서 처리할 수 있도록하는 ExceptionHandler
기능을 제공한다. @ExceptionHandler
어노테이션으로 사용할 수 있으며 value
값에 핸들러 함수에서 어떤 예외를 처리할 것인지 적어주면 된다.
@ExceptionHandler(value = { NullPointerException.class })
public ResponseEntity<Object> handleNullPointerException(Exception ex) { ... }
@ExceptionHandler(value = { IllegalArgumentException.class, NullPointerException.class, UsernameNotFoundException.class })
public ResponseEntity<Object> handleNullPointerException(Exception ex) { ... }
예외처리 핸들러 함수는 클래스 내부에서 사용될 수 있다. 클래스 내부에 핸들러를 정의한 경우 해당 클래스에서 발생하는 예외만을 처리한다. 만약 A Controller
에 핸들러 메서드를 정의해놓은 경우 Controller
뿐만 아니라 Controller
와 의존 관계를 맺고 있는 다른 계층들에서 발생한 예외도 처리할 수 있다. 하지만 B Controller
에서 발생하는 예외는 처리하지 못한다.
@RestController
@RequiredArgsConstructor
public class AController {
private final AService service;
...
@ExceptionHandler(value = { NullPointerException.class })
public ResponseEntity<Object> handleNullPointerException(Exception ex) { ... }
}
서비스를 구현하다보면 Controller
가 한 개가 아닐텐데.. 그럼 Controller
마다 하나씩 핸들러 메서드를 정의해줘야 할까😱 다행히도 스프링은 AOP
라는 좋은 기능을 제공하고, 예외 처리 로직을 분리해서 하나의 모듈처럼 사용할 수 있다. @ControllerAdvice
와 @RestControllerAdvice
를 이용해서 컴포넌트를 생성하고 예외처리 메서드를 작성해놓으면 모든 클래스에 전역적으로 적용이 가능하다. 뿐만아니라 annotations
속성을 통해 특정 Controller
에만 적용할 수 있다.
What is @ControllerAdvice ?
Specialization of @Component for classes that declare @ExceptionHandler, @InitBinder, or @ModelAttribute methods to be shared across multiple @Controller classes.
// 모든 컨트롤러에 예외처리 핸들러 적용
@RestControllerAdvice
public class ApiExceptionHandler {
...
}
// 특정 컨트롤러에 예외처리 핸들러 적용
@RestControllerAdvice(annotations = AController.class)
public class ApiExceptionHandler {
...
}
@RestControllerAdvice
는 @ControllerAdvice + @ResponseBody
를 의미하며 예외 처리의 결과로 리턴되는 값을 자동으로 JSON
형식으로 직렬화해서 보내준다.
@AllArgsConstructor
@Getter
public class ApiException {
private final String message;
private final HttpStatus httpStatus;
}
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(value = { IllegalArgumentException.class, NullPointerException.class, UsernameNotFoundException.class })
public ResponseEntity<Object> handleApiRequestException(Exception ex) {
ApiException apiException = new ApiException(
ex.getMessage(),
HttpStatus.BAD_REQUEST
);
return new ResponseEntity<>(
apiException,
HttpStatus.BAD_REQUEST
);
}
}
Ajax
를 통해 서비스로 요청을 했고, 서버에서 예외가 발생해서 핸들러 메서드에 의해 오류 응답이 반환됐다면 프론트 콘솔에서 출력해보면, 실제로 JSON
형식의 응답 메시지가 반환된 것을 확인할 수 있다.
$.ajax({
...
error: function (response) {
console.log(response.responseJSON);
}
});
{message: '해당되는 아이디(22)의 게시물이 없습니다.', httpStatus: 'BAD_REQUEST'}
테스트 도중에 예외가 발생해서 프론트에서 정상 출력이 안되는데 서버 콘솔에도 오류 내용이 찍히지 않았다. 확인해보니 예외처리 핸들러를 구현할때 핸들러를 적용할 클래스들 목록들 중에 NullPointerException.class
가 있었는데, 프로그램에서 예외처리를 하지 못한 부분에서 발생한 예외도 핸들러로 인터셉트되서 생기는 문제였다. 그래서 IllegalArgumentException
을 상속받는 예외 클래스인 ApiRequestException
을 하나 더 만들어서 우리가 직접적으로 발생시키는 예외에는 ApiRequestException
객체를 생성하도록 수정했다.
public class ApiRequestException extends IllegalArgumentException {
public ApiRequestException(String message) {
super(message);
}
public ApiRequestException(String message, Throwable cause) {
super(message, cause);
}
}
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(value = { ApiRequestException.class })
public ResponseEntity<Object> handleApiRequestException(Exception ex) {
ApiException apiException = new ApiException(
ex.getMessage(),
HttpStatus.BAD_REQUEST
);
return new ResponseEntity<>(
apiException,
HttpStatus.BAD_REQUEST
);
}
}
Article article = articleRepository.findById(id).orElseThrow(
() -> new ApiRequestException(String.format("해당되는 아이디(%d)의 게시물이 없습니다.", id))
);
📌 Spring Framework 5.3.13 API. "ControllerAdvice".
📌 Carrey. "Spring Handle Exception", Carrey's 기술 블로그, 30 Aug 2018.*
📌 rin. "[Spring] ExceptionHandler", keep going, 08 May 2020.
📌 우드콕. "ResponseEntity는 왜 사용하는 것이며 @RestControllerAdvice는 무엇일까.", 02 May 2021.