Spring Boot ErrorHandling에 대해서 알아 보겠다. 컨트롤러 단에서 발생하는 에러들을 처리해야할 일이 많아 진다. 그때 해당 컨트롤러 안에서 발생하는 에러들을 핸들링 하겠다는것이 @ExceptionHandler(‘exception.class’)이다. 해당 어노테이션을 붙인 함수를 컨트롤러 안에 선언하면 어노테이션에 추가한 exception에 대해서 핸들러 함수가 처리를 해준다. 코드를 확인 해 보자
@RestController("/api/v1")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping(path = "/user")
public ResponseEntity<Object> createUser(@Valid @RequestBody UserDTO createUserDTOJson) {
Long id = userService.createUser(createUserDTOJson);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("location", String.valueOf(id));
return new ResponseEntity<>(httpHeaders, HttpStatus.OK);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> ValidationExceptionHandle(MethodArgumentNotValidException methodArgumentNotValidException) {
BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
for (ObjectError objectError : bindingResult.getAllErrors()){
logger.error(objectError.getDefaultMessage());
}
return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY);
}
위와 같은 코드가 있을때 UserControllerd에서 발생하는 MethodArgumentNotValidException에 대해서 @ExceptionHandler 어노테이션이 붙은 ValidationExceptionHandle 함수가 처리하게 된다.
더 나아가서 우리는 여러 컨트롤러들이 동일한 예외를 낸다면 위처럼 에러를 처리하기엔 많은 중복 코드가 발생하게 된다. 이런 문제를 해결하기 위해 우리는 전역 컨트롤러에서 발생하는 동일한 에러를 처리하는 클래스를 만들고 이 클래스에서 모든 에러를 처리하도록 구현할 수 있다. 아래의 코드를 보자.
//유저 컨트롤러
@RestController("/api/v1")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping(path = "/user")
public ResponseEntity<Object> createUser(@Valid @RequestBody UserDTO createUserDTOJson) {
Long id = userService.createUser(createUserDTOJson);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("location", String.valueOf(id));
return new ResponseEntity<>(httpHeaders, HttpStatus.OK);
}
}
//RestController에서 발생하는 모든 에러를 담당하는 클래스 -------------------------------------------------------------------------------
@RestControllerAdvice(annotations = RestController.class)
public class RestControllerExceptionHandler {
private static final Logger logger =
LoggerFactory.getLogger(UserExceptionController.class);
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> ValidationExceptionHandle(MethodArgumentNotValidException methodArgumentNotValidException) {
BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
for (ObjectError objectError : bindingResult.getAllErrors()){
logger.error(objectError.getDefaultMessage());
}
return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY);
}
}
위에서 처리한 에러는 VaildException으로 파라메타들의 값이 유효하게 들어왔는지를 확인한다. 이러한 에러는 여러 컨트롤러에서 발생할 수 있으며 각 컨트롤러마다 만들 필요없이 한번에 처리하도록 만드는것이 좋다. 먼저 @ControllerAdvice (annotations = ‘class’) 어노테이션을 이용하여 (annotations = ‘class’)에서 선언한 어노테이션들을 가진 클래스들의 에러를 전역으로 관리한다. 에러를 핸들링 하는 부분은 위와 같다.
추가 적으로 객체에 대한 유효성 검증에 대해서 알아보자.
유효성 검증 라이브러리는 spring-boot-starter-validation
를 사용하였다. 간단하게 유효성을 검증할 Entity나 DTO에 아래와 같이 @Null이나 @NotBlank와 같이 해당하는 어노테이션을 작성해주면 된다. 이후 위에 코드 처럼 유효성 검증할 변수 앞에 @Vaild를 작성해주면 된다.
//어떤 변수에 대해서 유효성을 체크할건지 선언하는 부분 @Valid
public ResponseEntity<Object> createUser(**@Valid** @RequestBody UserDTO createUserDTOJson)
//어떻게 체크할건지 선언하는 부분 @Null, @NotBlank, @Min(value = 2, message = "so young...") 등...
public class UserDTO {
*@Null*
private Long id;
@NotBlank
private String userId;
@NotBlank
private String pw;
@NotBlank
private String name;
@Min(value = 2, message = "so young...")
private Integer age;
}
마지막으로 제대로 동작하는지 확인 하기위해 아래와 같은 테스트 코드를 작성.
@Test
public void createUser_ValidationExceptionTest() throws Exception {
//given
userDTO.setUserId(null);
userDTO.setPw("");
userDTO.setName(null);
userDTO.setAge(1); //Min Age 2
String userDTOJson = new ObjectMapper().writeValueAsString(userDTO);
given(userService.createUser(any(UserDTO.class))).willReturn(0L);
//when
ResultActions resultActions = mockMvc.perform(
post("/user")
.contentType(MediaType.APPLICATION_JSON)
.content(userDTOJson)
);
//then
resultActions
.andDo(print())
.andExpect(status().isUnprocessableEntity());
}
실행 결과
2020-12-17 02:59:44.935 ERROR 12088 --- [ Test worker] c.e.s.e.RestControllerExceptionHandler : must not be blank
2020-12-17 02:59:44.936 ERROR 12088 --- [ Test worker] c.e.s.e.RestControllerExceptionHandler : must not be blank
2020-12-17 02:59:44.936 ERROR 12088 --- [ Test worker] c.e.s.e.RestControllerExceptionHandler : too young...
2020-12-17 02:59:44.936 ERROR 12088 --- [ Test worker] c.e.s.e.RestControllerExceptionHandler : must not be blank
...
BUILD SUCCESSFUL in 6s
유효성 검사에서 에러를 발생했기 때문에 controller에서 422(unprocessableEntity)에러를 반환하여 정삭적으로 유효성 검사가 되는지 테스트 하였다.
Vaild 체크 어노테이션이나 @RestControllerAdvice의 옵션들은 문서를 참고하자