@Valid 애너테이션을 통해 우리는 Controller로 오는 정보들의 유효성을 검사 할 수 있다. 이 때 유효성 검사에서 통과하지 못하면 MethodArgumentNotValidException이 발생한다.
이 예외가 발생했을 때 대처하는 방법은 두가지가 있다.
하나는 Controller내부 mapping 메서드들 마다 매개변수로 BindingResult를 받아 hasErrors() 메서드를 사용해서 예외가 발생했는지 확인하고 후처리를 하는 방법.
@Controller
@RequestMapping(value = "/member")
public class TestController {
@PostMapping
@ResponseBody
public ResponseEntity saveMember(@Valid Member member, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(bindingResult.getAllErrors());
}
// member save code
return ResponseEntity.ok(member);
}
}
또 하나는 @ExceptionHandler 애너테이션을 사용해서 MethodArgumentNotValidException을 감지하도록 만들고 그에 따른 responseEntity를 만들어 처리하는 방법.
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ExceptionDTO> validationExceptionHandler(MethodArgumentNotValidException e){
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
ExceptionDTO errorResponse = new ExceptionDTO("false",400, errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
유효성 검사가 여러개 시행 될 경우 여러개를 담아야하기 때문에 위 코드에서는 Map에 에러요약을 담고 body에 담아 반환했다.
우리는 Controller에서 여러종류의 Exception을 만난다. NULL은 우리의 주적이며 특정한 상태일 때 예외를 처리해야할 경우 Exception을 직접 만들어 던지기도 하는데 이 때 @ExceptionHandler를 사용한다면 다양한 예외발생을 핸들링 할 수 있을 것이다.
그럼 이 ExceptionHandler를 모아 둘 수는 없을까?
왜 없겠나
@ControllerAdvice를 클래스레벨에 선언하면 그 클래스가 존재하는 디렉토리의 하위 디렉토리까지 Controller가 있다면 거기서 발생하는 모든 Exception을 그 클래스가 핸들링 할 수 있다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<BaseResponseDTO> runtimeExceptionHandler(Exception e){
BaseResponseDTO errorResponse = new BaseResponseDTO("false",400);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
// requestDTO 유효성 검사 실패
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ExceptionDTO> validationExceptionHandler(MethodArgumentNotValidException e){
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
ExceptionDTO errorResponse = new ExceptionDTO("false",400, errors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
// 비밀번호 불일치
@ExceptionHandler(PasswordMismatchException.class)
public ResponseEntity<ExceptionDTO> passwordMismatchExceptionHandler(PasswordMismatchException e){
Map<String, String> errors = Collections.singletonMap("error", e.getMessage());
ExceptionDTO errorResponse = new ExceptionDTO("false",401, errors);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}
// 유저 정보 없음
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ExceptionDTO> userNotFoundExceptionHandler(UserNotFoundException e){
Map<String, String> errors = Collections.singletonMap("error", e.getMessage());
ExceptionDTO errorResponse = new ExceptionDTO("false",404, errors);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
자매품 @RestControllerAdvice도 있다. 이름에서 알 수 있듯 @RestControllerAdvice는 @ResponseBody 이 있기 때문에 반환하는 객체를 body에 담아 JSON 형식으로 반환해준다.