지난 포스팅에서는 @Validated 어노테이션으로 검증을 구현해보았다. 유효성 검사를 위해, Validator 인터페이스 구현체를 만들고 검증 로직도 작성하였다. 그러나 대부분의 검증 로직은 비슷한 경우가 많다.
빈값인가? Null값인가? 허용범위를 초과했는가? 등
비슷한 검증 로직을 가진 검증기 구현체를 반복해서 생성해야 한다. 이런 반복을 막고자 JAVA는 어노테이션 형태로 검증 과정을 자동화 하였다. 이를 Bean Validation이라 부른다.
검증을 지원하는 어노테이션을 사용하려면 위 라이브러리를 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
SpringBoot 환경이라면 위 의존관계를 build.gradle에 추가하면 된다.
Bean Validation
지난 포스팅에서 검증기 구현체를 만들었다.
ItemValidator
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz); //Item을 검증할 수 있는 Validator임을 표시
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors,"itemName","required");
if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){
errors.rejectValue("price","range",new Object[]{1000,100000000},null);
}
if(item.getQuantity() == null || item.getQuantity() >= 9999){
errors.rejectValue("quantity","max",new Object[]{9999},null);
}
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice()*item.getQuantity();
if(resultPrice < 10000){
errors.reject("totalPriceMin", new Object[]{10000,resultPrice},null);
}
}
}
}
Item 클래스 전용 검증기이다. Bean Validation을 사용하지 않으면 각 클래스에 맞는 검증기를 따로 구현해야 한다. 대부분 비슷한 유효성 검사를 진행하기 위해, 각각의 검증기를 따로 구현하는 것은 소모적이다.
그러므로 BeanValidation을 사용해보자.
Controller
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult) {
// 중략 ...
}
컨트롤러 파라미터를 보자.
@Validated가 선언되어 있다. @Validated는 파라미터로 들어 온 객체에 대한 검증이 필요하다고 Sping에게 알리는 것이다. Spring은 해당 객체를 검증할 수 있는 Validator 검증기 구현체를 탐색한다. SpringBoot는 Bean Validation을 수행할 수 있는 Bean Validator를 자동으로 등록하는데, 이를 Global Validator라 부른다.
Bean Validation은 Global Validator가 동작으로 이루어진다. 필드에 선언된 어노테이션을 토대로, 필드에 주입된 데이터가 어노테이션에 설정된 유효성에 부합하는지를 검사한다. 부합하지 않는다면 BindingResult에 오류 내용을 담는다.
Item 클래스
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min=1000,max=1000000)
private Integer price;
@Max(9999)
private Integer quantity;
}
Item 클래스를 보면 필드에 유효성 검사가 가능하도록 어노테이션이 선언되어 있는 것을 볼 수 있다. @NotBlank는 빈값인 경우, @NotNull은 필드에 Null값이 들어간 경우, @Range는 필드에 주입된 데이터가 설정된 범위 밖에 있는 경우 BindingResult에 오류 내용을 담는다. 오류내용을 담을 때, 오류코드를 사용하는데 이는 다음 포스팅에서 다루어 보겠다. 타입이 일치하지 않아 바인딩에 실패한 경우에는 어노테이션에 의한 유효성검사는 동작하지 않는다. 바인딩 에러는 그대로 BindingResult에 담긴다.
그러므로 동작 순서는 아래와 같다.
1) @ModelAttribute가 선언된 파라미터의 객체를 생성하여 Request 데이터를 주입한다.
2) 주입과정에서 바인딩에러가 발생하면 BindingResult에 결과를 담는다.
3) 바인딩 에러가 발생하지 않은 필드만 Bean Validation을 적용한다.
Spring은 Bean Validation을 제공하는 다양한 어노테이션을 가지고 있다. 대부분의 검증로직은 구현되어 있으므로 위 링크를 참고 하면 된다. 참고로 하이버네이트인 이유는 Bean Validation 기능을 제공하는 Jakarta Bean Validation 인터페이스의 구현체가 하이버네이트이기 때문에 그렇다.
오브젝트 오류 ( 글로벌 오류 )
지금까지는 단일 필드 검증을 다루어 보았다. 만약 두 개 이상의 필드를 복합적인 로직으로 검증하려면 어떻게 해야 할까?
@Data
@ScriptAssert(lang="javascript", script="_this.price * _this.quantity >= 10000 ", message = " 총합이 10000원 넘게 입력해주세요")
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min=1000,max=1000000)
private Integer price;
@Max(9999)
private Integer quantity;
}
@ScriptAssert는 필드가 두 개 이상 엮인 오브젝트 단위 검증을 지원한다. 필드가 2개 이상 엮이면 '로직'이 필요하다. @ScriptAssert는 스크립트 언어, 검증 로직 그리고 검증 로직이 실패한 경우 메시지를 설정이 가능하다.
그런데 이는 실무에서 잘 사용하지 않는다.
이유는 스크립트 언어를 사용하기 때문이다. JAVA 언어와 스크립트 언어가 섞이면 가독성이 떨어지고 컴파일러가 스크립트 언어를 해석하지 못하니 타입 안정성이 떨어진다. 또한 이런 복합적인 검증은 오브젝트 범위 내에서만 일어나지 않는다. 오브젝트 범위 밖의 요소 연계되어 검증해야 하는 경우가 많다. 그러므로 @ScriptAssert로 처리하기 보다는 Controller 내부에서 JAVA 언어로 검증을 구현하는 것이 좋다.
Controller
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult) {
// Object Error의 경우 JAVA 코드로 따로 처리하는 것이 더 편리함
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice()*item.getQuantity();
if(resultPrice < 10000){
bindingResult.reject("totalPriceMin", new Object[]{10000,resultPrice},null);
}
}
// 검증에 실패하면 다시 입력폼으로 이동
if(bindingResult.hasErrors()){
log.info("errors={}",bindingResult);
return "validation/v3/addForm";
}
// 검증 성공
// [ 비즈니스 로직 ]
}
@Validated로 단일 필드에 주입된 데이터 검증이 완료되면 Controller 로직이 실행된다. 이때 검증이 완료된 필드 데이터를 가지고 복합 검증을 진행한다. 이는 JAVA 언어를 사용하기에 가독성이 올라가고 Item 범위 밖의 요소와 함께 검증 로직을 구현할 수 있다.
정리하면.
Bean Validation을 구현하면 검증기 구현체를 따로 구현하지 않아도 된다. @Validated(or @Valid)로 선언된 파라미터를 가진 메소드가 호출되면, Spring은 파라미터 클래스를 검증할 수 있는 검증기를 찾는다. Bean Validation을 수행할 수 있는 글로벌 검증기는 스프링부트가 자동으로 Bean으로 등록해놓는다. @ModelAttribute가 선언되어 Spring이 요청 데이터를 주입해 놓은 객체에 검증기는 검증을 시작한다. 선언된 어노테이션을 토대로 필드에 주입된 데이터가 유효성에 부합하는지를 검사하고 부합하지 않으면 결과를 BindingResult에 저장한다. Bean Validation은 필드 단위 검증에 적합하다. 오브젝트 단위의 검증은 따로 검증 로직을 구현하는 것이 좋다.
참고자료
'SPRING > Spring MVC' 카테고리의 다른 글
[SpringMVC] 검증(Validation)(7) - Bean Validation ( groups ) (0) | 2023.08.30 |
---|---|
[SpringMVC] 검증(Validation)(6) - Bean Validation ( 에러 코드 ) (0) | 2023.08.29 |
[SpringMVC] 검증(Validation)(4) - BindingResult ( @Validated ) (0) | 2023.08.28 |
[SpringMVC] 검증(Validation)(3) - BindingResult ( rejectValue 메소드 ) (0) | 2023.08.28 |
[SpringMVC] 검증(Validation)(2) - BindingResult ( FieldError ) (0) | 2023.08.28 |