[Spring] @Valid 이해하기

Sangwon·2023년 8월 2일

프로젝트에서 Controller 의 Parameter의 객체에서 검증이 필요할 때 Bean Validation 사용하고 있습니다.

팀원의 코드 리뷰를 진행하던 중, 헷갈리는 것들이 생겨 @Valid 에 대해 확실히 정리하고자 포스팅을 작성합니다.

시작하기에 앞서

스프링 부트가 Bean Validation에 대한 Validator 를 등록할 수 있도록 build.gradle에 의존 관계를 추가합니다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

Bean Validation

Bean Validation을 이용하는 것은 간단합니다.
스프링 부트에서 Bean Validation을 원하는 객체에 검증 애노테이션을 달면 됩니다.

먼저 예시 코드를 살펴보겠습니다.

public class Car {

   @NotNull
   private String manufacturer;

   @NotNull
   @Size(min = 2, max = 14)
   private String licensePlate;

   @Min(2)
   private int seatCount;

   // ...
}
  • @NotNull : 필드가 Null인지 검증합니다.
  • @Min() : int 와 같은 숫자 타입 필드에만 가능하며, 최솟값보다 같거나 큰 값임을 검증합니다.

@Valid 와 @Validated

앞서 Car 객체에 대해 검증이 필요하다면, 필요한 곳에 @Valid 또는 @Validated 를 사용하면 됩니다.

예시 코드를 살펴보겠습니다.

@PostMapping("/create")
public String createCar(@RequestBody @Valid Car car) {
	//.....
}

위의 코드처럼 Bean Validation 검증을 원하는 객체에 @Valid 또는 @Validated 애노테이션을 달면 됩니다.
이후 스프링에 등록된 Validator가 필드 별로 정의한 Bean Validation 을 통해 검증을 수행합니다.

그렇다면, @Validated는 @Valid 와 어떤 차이가 있을까요?

결론적으로 말한다면, @Validated는 스프링 부트에서 제공하는 기능으로 groups 기능을 제공합니다.

예시를 통해 접근해봅시다.

public class User {
    @NotBlank(groups = CreateUser.class)
    private String name;
    
    @Min(value = 18, groups = UpdateUser.class)
    private int age;
}

User 라는 클래스에서,

  • 이름은 User가 생성될 때 반드시 필요하지만, 수정될 때에는 반드시 필요한 것은 아닙니다.
  • 나이도 수정 과정에서만 최소 18세 이상임이 요구된다고 해봅시다.

핵심은, 하나의 필드에 대해 상황에 따라 다른 검증 기준이 필요하다는 점입니다.

이런 상황에서 스프링 부트의 @Validated 는 groups 기능을 통해 상황에 맞는 검증 기준을 적용할 수 있도록 합니다.
앞서 정의한 groups 에 해당하는 검증 기준이 필요하다면 Validated 안에 적어주면 됩니다.

public class UserService {

    public void createUser(@Validated(CreateUser.class) User user) {
        // logic to create a new user
    }

    public void updateUser(@Validated(UpdateUser.class) User user) {
        // logic to update an existing user
    }
}

하지만, 이렇게 하나의 필드에 대해 상황에 따라 다른 검증 기준이 필요한 경우
@Validated 가 제공하는 groups 기능을 쓰는 것보다는 상황에 맞는 객체를 분리하여 각각의 다른 객체로 만들고 나서 각각 검증 기준을 적용한 뒤에 @Valid를 수행하는 것이 권장됩니다.

위의 예시에서는 User를 CreateUser , UpdateUser 로 분리하고 각각에 맞는 Bean Validation을 적용하면 되겠습니다.

따라서 실제 프로젝트에서는 다음과 같이 전략을 결정하였습니다.

  • 검증 기준이 여러 개 필요하다면 객체를 분리하고
  • @Valid 만 사용

@Valid 검증 오류 발생 및 처리

@Valid 가 붙은 객체에서 검증 오류가 발생하면 어떻게 처리할 수 있을까요?

2가지 방식으로 처리할 수 있습니다.

BindingResult

다음 코드를 살펴봅시다.

@PostMapping("/create")
public String createItem(@ModelAttribute @Valid Item item, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        // 검증 실패 시 처리 로직
    }
    // ...
}

@Valid 검증 객체 바로 뒤에 BindingResult 객체를 두어 처리하는 방법이 있습니다.
스프링 부트는 @Valid 가 붙은 객체에서 Bean Validation에 실패할 경우 BindingResult
검증 실패에 관한 정보를 넣습니다
. 따라서 BindingResult 를 이용해 검증 실패 로직을 필요에 맞추어 작성하면 됩니다.

MethodArgumentNotValidException

만약 BindingResult 가 명시적으로 없는데 @Valid가 실패하면 어떻게 될까요?

스프링 부트는 MethodArgumentNotValidException 예외를 발생시킵니다.
따라서, @RestControllerAdvice 등을 통해 공통 예외 처리를 수행하거나, 컨트롤러 안에서 try-catch
등을 활용하여 예외 발생 시 처리 로직을 작성하면 됩니다.

결론

프로젝트에서는 MethodArgumentNotValidException이 발생하도록 했고,
@RestControllerAdvice를 활용해 @Valid에 대한 검증 실패를 공통으로 처리해주었습니다.

@ModelAttribute, @RequestBody 에서의 @Valid

개인적으로 조금 헷갈리는 부분이 있어 따로 정리해보았습니다!

@ModelAtrribute 와 @RequestBody는 편리하게 객체를 만든다는 점에서는 같지만 그 방식이 다릅니다.

@ModelAttribute

  • Request Parameter 또는 HTML Form 에서 객체를 만듭니다.
  • WebDataBinder에 의해 객체가 생성됩니다.
  • 객체의 필드 단위로 검증이 적용됩니다.

@RequestBody

  • Message Body에 있는 JSON을 통해서 객체를 만듭니다.
  • HttpMessageConverter에 의해 객체가 생성됩니다.
  • 객체가 모두 만들어지고 나서, 검증이 적용됩니다.

핵심적인 차이는 마지막에 있었습니다.
@ModelAttribute는 객체가 완전히 생성되지 않아도 필드 별로 검증이 일어났고,
@RequestBody는 객체가 완전히 생성이 되어야만 검증이 진행되었습니다.

결론

  • 두 방식이 검증이 적용되는 타이밍이 다르다.
  • 하지만, 검증에 실패하는 경우 BindingResult 또는 MethodArgumentNotValidException 의 2가지 방식으로 검증 실패 처리하는 것은 같다.
  • @RequestBody의 경우 객체 자체가 만들어지지 않으면 검증 실패가 아닌 다른 오류가 발생하므로 별도의 예외 처리가 필요합니다.
profile
컴퓨터공학을 전공하였고, 현재는 금융업에 종사하며 투자에 관심이 많습니다.

0개의 댓글