API서버를 구축할때에는 서버에서 예상치 못한 치명적인 오류가 발생하는 경우(ex: nullpointerException)가 아니라 클라이언트 쪽에서 API의 요구사항과 맞지 않는 잘못된 요청으로 인해 발생하는 에러는 모두 400 bad request 응답 처리를 해주어야 한다.
이때 스프링 부트에서 개발자의 편의를 위해서 미리 만들어서 제공해주는 HandlerExceptionResolver를 사용하여 응답을 내려주면 된다.
WebMvcConfigurer에 등록 없이 어노테이션을 붙여주는 것 만으로 사용이 가능하여 상당히 유연하게 사용할 수 있는 handlerExceptionResolver이다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
원래는 위 어노테이션을 이용하면 ResponseStatusHandlerExceptionResolver가 동작하기 때문에 sendError를 통해 400 에러를 보내주고, BasicErrorController에서 에러를 처리하지만@ExceptionHandler와 사용하면 @ExceptionHandler가 스프링이 등록해주는 HandlerExceptionResolver 중에서 더 높은 우선순위를 가지고 동작하기 때문에 @ResponseStatus는 Resolver를 통한 동작은 무시되고 단순히 상태 코드를 설정하는 용도로 사용된다.
@ExceptionHandler(IllegalArgumentException.class)
위 어노테이션을 예외처리 메서드에 달아주고 특정 exception 클래스를 지정해주면 해당 예외에만 자동으로 매핑되어 동작하게 된다.
@ControllerAdvice, @RestControllerAdvice
위 어노테이션이 들어간 exception handler를 컨트롤러마다 달아주어도 되지만 관심사의 분리를 위해 exception과 controller의 처리영역을 분리하기 위해 도움을 주는 어노테이션이다.
@ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해주는 역할을 한다. 즉, 클래스를 분리해서 따로 관리할 수 있다. @ControllerAdvice는 대상을 지정하지 않으면 모든 컨트롤러에 적용된다(Global).
유효성 검사가 필요한 DTO
public class ProductRequestDto {
@NotBlank(message = "상품 이름은 공백일 수 없습니다.", groups = PostValidationGroup.class)
private String name;
@NotBlank(message = "상품 설명은 공백일 수 없습니다.", groups = PostValidationGroup.class)
private String description;
}
@Validated으로 유효성 검증을 하는 컨트롤러
@PostMapping("/newProduct")
public void uploadNewProduct(@RequestBody @Validated(PostValidationGroup.class)
ProductRequestDto productReqeustDto,
BindingResult bindingResult) {
// 빈값이 들어왔는지 유효성 검사
InputValidator.BadRequestHandler(bindingResult);
mainService.uploadNewProduct(productReqeustDto);
}
Binding Result를 확인하는 클래스
public class InputValidator {
public static void BadRequestHandler(BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
List<ObjectError> errorList = bindingResult.getAllErrors();
StringBuilder errorMessages = new StringBuilder();
for(ObjectError error : errorList) {
//돌면서 에러를 전부 모아서 에러 메세지 리스트로 응답식으로 수정
System.out.println(error.getDefaultMessage());
errorMessages.append(error.getDefaultMessage()).append("\n");
}
throw new IllegalArgumentException(errorMessages.toString());
}
}
}
에러 데이터를 내려줄 객체 클래스
@Data
@AllArgsConstructor
public class ErrorResult {
private HttpStatus code;
private String message;
}
400 Bag request를 내려주기 위한 예외 처리 클래스
@Slf4j
@RestControllerAdvice(assignableTypes = MainController.class) // 지정하지 않으면 글로벌
public class ControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExResolver(IllegalArgumentException e) {
log.info("illegalExResolver Start!");
return new ErrorResult(HttpStatus.BAD_REQUEST,
e.getMessage());
}
}
참고자료: