이번 포스트에서는 스프링의 예외처리에 대해 정리해보았다.
스프링의 기본 예외처리 방식
다음과 같은 예시코드가 있다.
@Service
public class MemberService implements IMemberService {
private MemberDAO memberDAO; // null
public MemberService() {}
//@Autowired
//public MemberService(MemberDAO memberDAO) {
// this.memberDAO = memberDAO;
//}
...
}
원래라면 MemberDAO가 Autowired를 통해 외부에서 객체가 주입이 되어야 하는데 그러지 못하여 null인 상태이다. 이 상태에서 고객이 회원가입을 하면 내부에서는 데이터베이스에 접근하여 회원 가입 서비스를 처리해야하는데 데이터베이스 접근 객체가 null인 상태라 NullPointerException 예외가 발생한다.
[예외 발생 결과]

이 예외를 보면 컨트롤러 하위에서 예외가 발생했을 때 별도의 예외처리를 하지 않으면 WAS까지 에러를 전달한다는 것을 알 수 있다. 왜냐하면 이 에러 페이지는 Tomcat에 내장되어 있는 에러페이지 이기 때문이다.
@ResponseStatus
@ResponseStatus는 에러 HTTP Status 상태를 변경하도록 도와주는 어노테이션이다.
@ResponseStatus은 다음과 같이 사용할 수 있다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
// InputInvalidException -> 400 -> WAS -> erorr/400.jsp
public class InputInvalidException extends Exception {
public InputInvalidException() {
super();
}
public InputInvalidException(String message) {
super(message);
}
}
Exception 클래스에 어노테이션을 붙이면 ResponseStatusExceptionResolver가 @ResponseStatus를 처리한다. 해당 예외를 WAS까지 전달시키고 복잡한 WAS 에러 요청 전달이 진행된다.
- 한계점
- 별도의 응답 상태가 필요하면 예외 클래스를 추가해야 한다.
- 예외 클래스와 강하게 결합되어있어 같은 예외는 같은 상태와 에러 메시지를 반환한다.
- WAS까지 예외가 전달되고 WAS 에러 요청 전달이 진행된다.
- 외부에서 정의한 예외 클래스는 @ResponseStatus 사용할 수 없다.
ResponseStatusException
스프링 5.0에서 @ResponseStatus의 프로그래밍 대안으로 손쉽게 에러를 반환할 수 있는 ResponseStatusException이 추가되었다. ResponseStatusException은 HttpStatus와 함께 선택적으로 reason과 cause를 추가할 수 있다. unchecked exception을 상속하고 있어 명시적으로 에러 처리하지 않아도 된다.
깨알 - checked exception과 unchecked exception ?
- checked exception
- RuntimeException의 하위 클래스가 아니면서 Exception 클래스의 하위 클래스들을 의미
- 반드시 예외 처리를 해야함 (try/catch)
- unchecked exception
- RuntimeException의 하위 클래스들을 의미 (말 그대로 실행 중에(runtime) 발생할 수 있는 예외를 의미)
- 예외 처리를 강제하지 않음
사용법
@RestController
@RequestMapping("")
public class SignupRestController {
...
@PostMapping("/signup")
public ResponseEntity<Status> doSignup(@ModelAttribute MemberDTO memberDTO, BindingResult bindingResult) throws InputEmptyException, DatabaseDuplicateException, InputInvalidException {
try {
memberService.signup(memberDTO.getuId(), Password.of(memberDTO.getuPwStr()), memberDTO.getuEmail());
return new ResponseEntity<>(Status.SUCCESS, HttpStatus.OK);
} catch (NullPointerException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "BAD REQUEST");
}
}
...
}
@ResponseStatus와 동일하게 예외 발생시 ResponseStatusExceptionResolver가 에러를 처리
장점
- 기본적인 예외 처리를 빠르게 적용할 수 있으므로 손쉽게 프로토타이핑할 수 있음
- HttpStatus를 직접 설정하여 예외 클래스와의 결합도를 낮출 수 있음
- 불필요하게 많은 별도의 예외 클래스를 만들지 않아도 됨
- 프로그래밍 방식으로 예외를 직접 생성하므로 예외를 더욱 잘 제어할 수 있음
한계점
- 직접 예외 처리를 프로그래밍하므로 일관된 예외 처리가 어려움
- 예외 처리 코드가 중복될 수 있음
- Spring 내부의 예외를 처리하는 것이 어려움
- 예외가 WAS까지 전달되고 WAS의 에러 요청 전달이 진행됨
ExceptionHandler
@ExceptionHandler는 어노테이션을 통해 에러를 매우 유연하고 쉽게 처리할 수 있게 도와준다.
사용법
- @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드에 사용한다.
@RestController
@RequestMapping("")
public class SignupRestController {
...
// NullPointerException 발생할 수 있는 컨트롤러 함수
...
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<Status> handleNullPointerException(NullPointerException exception) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMessage());
}
...
}
Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다. @ExceptionHandler 예외 클래스를 지정하지 않으면 파라미터에 설정된 에러 클래스를 처리하게 된다.
@ResponseStatus와 결합 가능하다 (ResponseEntity에서도 status를 지정하고, @ResponseStatus가 있다면 ResponseEntity가 우선이다.)
- 장점
- @ResponseStatus와 달리 에러 응답 (payload)을 자유롭게 다룰 수 있음
- 응답을 다음과 같이 정의해서 내릴 수 있음
- code - 어떠한 종류의 에러가 발생하는지에 대한 에러 코드
- message - 왜 에러가 발생했는지에 대한 설명
- errors - 어느 값이 잘못되어 @Valid에 의한 검증이 실패한 것인지를 위한 에러 목록
@ControllerAdvice, @RestControllerAdvice
스프링은 전역적으로 @ExceptionHandler를 적용할 수 있는 컨트롤러를 제공한다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ NullPointerException.class, InputEmptyException.class, InputInvalidException.class, DatabaseDuplicateException.class })
public ModelAndView handleExceptions(HttpServletRequest request, Exception exception) {
System.out.println(request.getRequestURI() + " raised " + exception);
ModelAndView modelAndView = new ModelAndView("error/error");
modelAndView.addObject("exception", exception);
modelAndView.addObject("url", request.getRequestURL());
return modelAndView;
}
}
장점
- 하나의 클래스로 모든 컨트롤러에 대해 전역적으로 예외처리 가능함
- 직접 정의한 에러 응답을 일관성 있게 클라잉너트에게 내려줄 수 있음
- 별도의 try-catch 구문이 없어 코드의 가독성이 높음
주의사항
- 여러 ControllerAdvice가 있을 때 @Order로 순서를 지저하지 않으면 임의의 순서로 처리함
- 일관된 예외 응답을 위해 한 프로젝트당 하나의 ControllerAdvice를 관리하는 것이 좋음
- 여러개의 ControllerAdvice가 필요하면 basePackages나 annotations 등을 지정해야 함
- 직접 구현한 예외 클래스는 한 공간에서 관리하는 것이 보편적임
스프링 예외처리 흐름

스프링에는 다음과 같은 예외처리 전략이 스프링 빈으로 등록되어 있다.
- ExceptionHandlerExceptionResolver 스프링 디폴트 전략
- 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
- 컨트롤러 내부에서 발생하는 예외처리
- ResponseStatusExceptionResolver 스프링 디폴트 전략
- Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
- 컨트롤러 내부에서 발생하는 예외처리
- DefaultHandlerExceptionResolver 스프링 디폴트 전략
- 스프링 내부의 기본 예외들을 처리함
- 스프링에서 발생하는 주요 예외를 처리하는 표준 예외 처리기 (400, 404, 500)
- 스프링 내부에서 발생하는 예외를 처리하기 위한 용도이므로 우리가 신경쓸 필요없음
- 컨트롤러 내부에서 발생하는 예외처리
- SimpleMappingExceptionResolver
- web.xml에 error-page를 지정하여 특정 예외를 처리할 뷰를 매핑
- exception 별로 error-page를 매핑할 수 있는 기능 제공
- 실제로 사용하기 가장 편리함
- 사용자에게 부담스러운 HTTP 상태코드와 예외 메시지를 던지지 않음
- 친절하게 예외 페이지를 보여주는 것이 좋음
- 컨트롤러 외부 발생하는 예외처리
- 만약 Dispatcher Servlet 이전에 Filter에서 예외처리가 난다면 Web Application 레벨에서 에러를 처리해야 함
- 예외 발생시 로그를 남기거나 관리자에게 통보하는 것이 어려움
- 디폴트 전략이 아니므로 빈을 등록해야 함
- 예시 코드
@Configuration @EnableWebMvc public WebMvcConfig extends WebMvcConfigurerAdapter { /* * simple 예외 리졸버 * */ @Bean(name = "simpleMappingExceptionResolver") public SimpleMappingExceptionResolver simpleMappingExceptionResolver() { SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver(); Properties mapping = new Properties(); mapping.setProperty("NullPointerException", "error/nullError"); mapping.setProperty("ArrayIndexOutOfBoundsException", "error/arrayBoundsError"); mapping.setProperty("ArithmeticException", "error/arithmeticError"); simpleMappingExceptionResolver.setExceptionMappings(mapping); simpleMappingExceptionResolver.setDefaultErrorView("error/error"); // 등록되지 않은 exception에 보여줄 뷰 return simpleMappingExceptionResolver; } }
예외처리 흐름

- ExceptionHandlerExceptionResolver 동작
- 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사함
- 컨트롤러의 @ExceptionHandler에서 처리가능하다면 처리하고, 그렇지 않으면 ControllerAdvice로 넘어감
- ControllerAdvice안에 적합한 @ExceptionHandler가 있는지 검사하고 없으면 다음 처리기로 넘어감
- ResponseStatusExceptionResolver 동작
- @ResponseStatus가 있는지 또는 ResponseStatusException인지 검사함
- 맞으면 ServletResponse의 sendError()로 예외를 서블릿까지 전달되고, 서블릿이 BasicErrorController로 요청을 전달함
- DefaultHandlerExceptionResolver 동작
- Spring 내부 예외인지 검사하여 맞으면 에러를 처리하고 아니면 넘어감
'Spring' 카테고리의 다른 글
Spring - ConnectionPool (0) | 2022.11.21 |
---|---|
Spring - Log4j2 (0) | 2022.11.21 |
Spring - JUnit+Mock 기반 스프링 단위테스트 (0) | 2022.11.21 |
Spring - ResponseEntity, RequestEntity (0) | 2022.11.21 |
Spring - @RestController (0) | 2022.11.21 |
댓글