@Valid를 사용하여 요청값을 검증할 때 검증을 통과하지 못하면 응답값이 너무 많이 나오는 문제가 있습니다.
요청 값 유효성 검사를 통과하지 못했을 때 예외 처리하는 방법을 설명드리겠습니다.
MethodArgumentNotValidException
우선 예외처리를 위해서는 요청값 검증에 실패했을 때 발생하는 예외가 뭔지 알아야 합니다.
요청값 유효성 검사를 통과하지 못하면 다음과 같은 로그를 확인할 수 있습니다.
다음은 @NotBlank 검사를 통과하지 못했을 때 출력되는 로그입니다.
2024-05-26T11:49:31.985+09:00 WARN 20448 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.demo1.ValidAnnotationController$ValidObject com.example.demo1.ValidAnnotationController.checkNotEmptyPost(com.example.demo1.ValidAnnotationController$ValidObject): [Field error in object 'validObject' on field 'string': rejected value [ ]; codes [NotBlank.validObject.string,NotBlank.string,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validObject.string,string]; arguments []; default message [string]]; default message [String is Not Blank]] ]
로그를 확인해보면 MethodArgumentNotValidException이라는 예외를 확인할 수 있습니다.
이 예외가 요청값 검증에 실패했을 때 발생하는 예외입니다.
그래서 이 예외를 ControllerAdvice가 선언된 클래스에 추가해주면 예외처리를 할 수 있습니다.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.AllArgsConstructor;
import lombok.Getter;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Response> methodArgumentNotValidException(MethodArgumentNotValidException e) {
return ResponseEntity
.badRequest()
.body(new Response("E", e.getMessage()));
}
@Getter
@AllArgsConstructor
public static class Response {
private String code;
private String message;
}
}
MethodArgumentNotValidException이 발생했을 때 에러코드와 메시지를 반환하였습니다.
그런데 이 방법 역시 검증해야 할 값이 많아지면 에러 메시지가 길어지는 문제점이 있습니다.
BindException
유효성 검증을 통과하지 못한 값이 있을 때 BindException을 발생시키는 방법이 있습니다.
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@RestController
public class ValidAnnotationController {
@PostMapping("/valid/exception")
public ValidObject bindException(@Valid @RequestBody ValidObject object, BindingResult result) throws BindException {
if (result.hasErrors()) {
throw new BindException(result);
}
return object;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class ValidObject {
@NotNull(message = "String is Not Null")
private String string;
@NotNull(message = "Integer is Not Null")
private Integer integer;
}
}
Controller 매핑 메소드에 BindingResult 객체를 사용하면 검증을 통과 못했을 때 컨트롤러 단에서 예외 처리를 진행할 수 있습니다.
ControllerAdvice에 BindException에 대한 예외처리 코드를 추가합니다.
⚠️이전에 추가한 MethodArgumentNotValidException 예외 처리 코드는 제거해야 합니다.
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.AllArgsConstructor;
import lombok.Getter;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
public ResponseEntity<Response> bindException(BindException e) {
return ResponseEntity
.badRequest()
.body(new Response("BE", "Bind Exception", e.getFieldErrors()));
}
@Getter
@AllArgsConstructor
public static class Response {
private String code;
private String message;
private List<FieldError> errors;
}
}
org.springframework.validation 패키지에 있는 BindException을 사용하면 됩니다.
에러 반환용 객체에 FieldError 리스트를 추가해서 어느 필드에서 에러가 발생했는지 확인할 수 있게 했습니다.
FieldError 객체를 그대로 반환하면 ExceptionHandler 적용 전과 큰 차이가 없어집니다.
에러 반환값을 정리해보겠습니다.
반환 값 정리
반환 값 중 defaultMessage와 rejectedValue만 반환하면 깔끔하게 반환할 수 있을 것 같습니다.
ExceptionHandler를 사용하는 메소드 코드를 수정합니다.
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.example.demo1.handler.GlobalExceptionHandler.Response.ErrorField;
import lombok.AllArgsConstructor;
import lombok.Getter;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
public ResponseEntity<Response> bindException(BindException e) {
List<FieldError> fieldErrors = e.getFieldErrors();
List<Response.ErrorField> errorFields = new ArrayList<>();
for (FieldError fieldError : fieldErrors) {
errorFields.add(new Response.ErrorField(fieldError.getRejectedValue(), fieldError.getDefaultMessage()));
}
// Stream map 사용
// List<ErrorField> errorFields = fieldErrors.stream()
// .map(fieldError -> new ErrorField(
// fieldError.getRejectedValue(),
// fieldError.getDefaultMessage()))
// .collect(Collectors.toList());
return ResponseEntity
.badRequest()
.body(new Response("BE", "Bind Exception", errorFields));
}
@Getter
@AllArgsConstructor
public static class Response {
private String code;
private String message;
private List<ErrorField> errors;
@Getter
@AllArgsConstructor
public static class ErrorField {
private Object value;
private String message;
}
}
}
FieldError 객체에서 필요한 값을 가져오기 위한 ErrorField 객체를 생성했습니다.
그리고 BindException 발생 시 객체를 ErrorField 객체로 변환합니다.
유효성 검사 메시지와 통과 못한 값만 반환되었습니다.
읽으면 좋은 글
[Java] Spring Boot 3 @RestControllerAdvice @ExceptionHandler 사용법