이번 포스트에서는 스프링의 예외처리에 대해 정리해보았다.
스프링의 기본 예외처리 방식
다음과 같은 예시코드가 있다.
@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 에러 요청 전달이 진행된다.
ResponseStatusException
스프링 5.0에서 @ResponseStatus의 프로그래밍 대안으로 손쉽게 에러를 반환할 수 있는 ResponseStatusException이 추가되었다. ResponseStatusException은 HttpStatus와 함께 선택적으로 reason과 cause를 추가할 수 있다. unchecked exception을 상속하고 있어 명시적으로 에러 처리하지 않아도 된다.
깨알 - checked exception과 unchecked exception ?
사용법
@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가 에러를 처리
장점
한계점
WAS
까지 전달되고 WAS
의 에러 요청 전달이 진행됨ExceptionHandler
@ExceptionHandler는 어노테이션을 통해 에러를 매우 유연하고 쉽게 처리할 수 있게 도와준다.
사용법
@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)을 자유롭게 다룰 수 있음@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
구문이 없어 코드의 가독성이 높음주의사항
스프링 예외처리 흐름
스프링에는 다음과 같은 예외처리 전략이 스프링 빈으로 등록되어 있다.
스프링 디폴트 전략
내부
에서 발생하는 예외처리스프링 디폴트 전략
내부
에서 발생하는 예외처리스프링 디폴트 전략
내부
에서 발생하는 예외처리외부
발생하는 예외처리@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
동작ResponseStatusExceptionResolver
동작DefaultHandlerExceptionResolver
동작