@Valid 예외를 전역 컨트롤러로 간단하게 처리하기

안영진·2019년 8월 7일
5

Spring을 사용하면 입력값을 받을 때 @Valid를 사용해서 쉽게 검증을 할 수 있는데 기본적으로 반환하는 에러 메시지는 너무 길고 복잡해 필요에 따라 처리하는 방법을 알아보았습니다.

우선 간단한 POST 요청을 처리하는 과정을 살펴보겠습니다.

HTTP POST /api/user
{
  "name": "hello",
  "age": 20
}

이와 같은 요청을 처리하기 위해 다음과 같은 Controller를 작성합니다.

@PostMapping("/api/user")
@ResponseBody
public String joinUser(@RequestBody User user) {
  /* user 정보 저장 */
  return "success";
}

입력받는 값을 검증하고 싶다면 User 클래스에서 Annotation을 통해 명시하고 Controller의 매개변수 앞에 @Valid를 붙여 편리하게 검사할 수 있습니다.

class User {

  @NotBlank   // name은 비어있지 않은 문자열만 허용
  String name;

  @Min(0)     // age는 0 이상의 값만 허용
  int age;

}

@PostMapping("/api/user")
@ResponseBody
public String joinUser(@RequestBody @Valid User user) { // @Valid 추가
  /* user 정보 저장 */
  return "success";
}

이제 해당 요청 시 정의한 Validation을 어길 경우 MethodArgumentNotValidException이 발생하며 다음과 같은 결과를 반환합니다.

HTTP POST /api/user
{
  "name": "hello",
  "age": -1         // @Min(0) 위반
}

반환값
{
  "timestamp": "2019-06-15T23:26:33.542+0000",
  "status": 400,
  "error": "Bad Request",
  "errors": [
    {
      "codes": [
        "NotBlank.user.name",
        "NotBlank.name",
        "NotBlank"
      ],
      "arguments": [
        {
          "codes": [
            "user.name",
            "name"
          ],
          "arguments": null,
          "defaultMessage": "name",
          "code": "name"
        }
      ],
      "defaultMessage": "반드시 값이 존재하고 공백 문자를 제외한 길이가 0보다 커야 합니다.",
      "objectName": "user",
      "field": "name",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotBlank"
    }
  ],
  "message": "Validation failed for object='user'. Error count: 1",
  "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String me.hello.blog.SimpleController.joinUser(me.hello.blog.User): [Field error in object 'user' on field 'name': rejected value [null]; 
  codes [NotBlank.user.name,NotBlank.name,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; 
  arguments []; default message [name]]; 
  default message [반드시 값이 존재하고 공백 문자를 제외한 길이가 0보다 커야 합니다.]]
  ...(생략)

굉장히 길고 자세한 결과가 반환되지만 경우에 따라 간단한 결과만 반환할수도 있기 때문에 Controller의 예외를 처리할 수 있는 ExceptionHandler로 해당 메시지를 줄여보도록 하겠습니다.

Controller내에 정의해도 되고 전역 설정을 위해 @ControllerAdvice를 정의해도 되는데 여기서는 @ControllerAdvice를 사용해서 처리해 보도록 하겠습니다.

[ ExceptionAdvisor.java ]

@ControllerAdvice   // 전역 설정을 위한 annotaion
@RestController
public class ExceptionAdvisor {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String processValidationError(MethodArgumentNotValidException exception) {
        BindingResult bindingResult = exception.getBindingResult();

        StringBuilder builder = new StringBuilder();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            builder.append("[");
            builder.append(fieldError.getField());
            builder.append("](은)는 ");
            builder.append(fieldError.getDefaultMessage());
            builder.append(" 입력된 값: [");
            builder.append(fieldError.getRejectedValue());
            builder.append("]");
        }

        return builder.toString();
    }

}

BindingResultFieldError에서 검증이 실패한 필드명, 실패에 대한 메시지, 검증이 실패한 입력값을 가져와 메시지를 만들어 반환하고 있습니다.

위의 코드를 작성한 뒤의 반환값은 다음과 같이 나옵니다.

[age](은)는 반드시 0보다 같거나 커야 합니다. 입력된 값: [-1]

훨씬 간결해진 메시지를 볼 수 있습니다. 물론 이렇게 바꾸는 것이 정답이 아니니 필요에 따라 반환하는 메시지를 처리하면 됩니다.

4개의 댓글

comment-user-thumbnail
2020년 6월 30일

안녕하세요, 글 잘보고 있습니다. 웹개발시 헤더값을 위의 반환 형식처럼 보여주는 프로그램을 따로 사용하시나요? 궁금하네요.

1개의 답글
comment-user-thumbnail
2021년 3월 16일

좋네요!

답글 달기