Study/spring

[Java] Spring Boot 3 @Valid ExceptionHandler 예외 처리 방법

 

@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 예외 처리 결과
MethodArgumentNotValidException 예외 처리 결과

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 리스트를 추가해서 어느 필드에서 에러가 발생했는지 확인할 수 있게 했습니다.

BindException 예외 처리 결과
BindException 예외 처리 결과

 

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 객체로 변환합니다.

BindException 발생 시 사용자 정의 객체 사용 결과
BindException 발생 시 사용자 정의 객체 사용 결과

유효성 검사 메시지와 통과 못한 값만 반환되었습니다.

 

 

읽으면 좋은 글

[Java] Spring Boot 3 @RestControllerAdvice @ExceptionHandler 사용법

 

[Java] Spring Boot 3 @RestControllerAdvice @ExceptionHandler 사용법

스프링에는 예외 처리를 위해 @ExceptionHandler와 @ControllerAdvice, @RestControllerAdvice 어노테이션이 존재합니다.이 어노테이션을 사용하는 이유와 예제를 소개해드리겠습니다. ExceptionHandler를 사용하는

priming.tistory.com