
프로젝트에서 Controller 의 Parameter의 객체에서 검증이 필요할 때 Bean Validation을 사용하고 있습니다.
팀원의 코드 리뷰를 진행하던 중, 헷갈리는 것들이 생겨 @Valid 에 대해 확실히 정리하고자 포스팅을 작성합니다.
스프링 부트가 Bean Validation에 대한 Validator 를 등록할 수 있도록 build.gradle에 의존 관계를 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-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;
// ...
}
앞서 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 라는 클래스에서,
핵심은, 하나의 필드에 대해 상황에 따라 다른 검증 기준이 필요하다는 점입니다.
이런 상황에서 스프링 부트의 @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 가 붙은 객체에서 검증 오류가 발생하면 어떻게 처리할 수 있을까요?
2가지 방식으로 처리할 수 있습니다.
다음 코드를 살펴봅시다.
@PostMapping("/create")
public String createItem(@ModelAttribute @Valid Item item, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 검증 실패 시 처리 로직
}
// ...
}
@Valid 검증 객체 바로 뒤에 BindingResult 객체를 두어 처리하는 방법이 있습니다.
스프링 부트는 @Valid 가 붙은 객체에서 Bean Validation에 실패할 경우 BindingResult에
검증 실패에 관한 정보를 넣습니다. 따라서 BindingResult 를 이용해 검증 실패 로직을 필요에 맞추어 작성하면 됩니다.
만약 BindingResult 가 명시적으로 없는데 @Valid가 실패하면 어떻게 될까요?
스프링 부트는 MethodArgumentNotValidException 예외를 발생시킵니다.
따라서, @RestControllerAdvice 등을 통해 공통 예외 처리를 수행하거나, 컨트롤러 안에서 try-catch
등을 활용하여 예외 발생 시 처리 로직을 작성하면 됩니다.
프로젝트에서는 MethodArgumentNotValidException이 발생하도록 했고,
@RestControllerAdvice를 활용해 @Valid에 대한 검증 실패를 공통으로 처리해주었습니다.
개인적으로 조금 헷갈리는 부분이 있어 따로 정리해보았습니다!
@ModelAtrribute 와 @RequestBody는 편리하게 객체를 만든다는 점에서는 같지만 그 방식이 다릅니다.
핵심적인 차이는 마지막에 있었습니다.
@ModelAttribute는 객체가 완전히 생성되지 않아도 필드 별로 검증이 일어났고,
@RequestBody는 객체가 완전히 생성이 되어야만 검증이 진행되었습니다.