Bean Validation
Bean Validation은 특정 구현체가 아니라 Bean Validation 2.0이라는 기술 표준이다. 즉, 검증 애노테이션과 여러 인터페이스의 모음이다.
BeanValidation을 구현한 기술 중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다.
Bean Validation 의존 관계 추가
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
Jakarta Bean Validation
jakarta.validation-api : Bean Validation 인터페이스
hiberate-validator 구현체
예시 코드
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
검증 애노테이션
@NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
@NotNull : null을 허용하지 않는다.
@Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
@Max(9999) : 최대 9999까지만 허용한다.
참고
javax.validation.constraints.NotNull
org.hibernate.validator.constraints.Range
javax.validation으로 시작하면 특정 구현에 관계 없이 제공되는 표준 인터페이스이고, org.hibernate.validator로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증 기능이다. 실무에서 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용해도 된다.
검증기 생성
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
검증 실행
Set<ConstraintViolation<Item>> violations = validator.validate(item);
스프링 부트는 자동으로 글로벌 Validator로 등록한다.
LocalValidatorFactoryBean을 글로벌 Validator로 등록한다. 이 Validator는 @NotNull 같은 애노테이션을 보고 검증을 수행한다. 이렇게 글로벌 Validator가 적용되어 있기 때문에 @Valid, @Validated만 적용하면 된다. 검증 오류가 발생하면, FieldError, ObjectError를 생성해서 BindingResult에 담아준다.
주의
직접 글로벌 Validator를 등록하면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않는다.
검증 순서
- @ModelAttribute 각각의 필드에 타입 변환 시도
- 성공하면 다음으로
- 실패하면 typeMismatch로 FieldError 추가
- Validator 적용
바인딩에 성공한 필드만 Bean Validation 적용
BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다. 타입 변환에 실패한 필드는 Bean Validation 적용이 의미 없기 때문.
BeanValidation 메시지 찾는 순서
- 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
- 애노테이션의 message 속성 사용 → @NotBlank(message = "공백! {0}")
- 라이브러리가 제공하는 기본 값 사용 → 공백일 수 없습니다.
애노테이션의 message 사용 예
@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
Object Error
@ScriptAssert() 사용
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
// ...
}
메시지 코드
- ScriptAssert.item
- ScriptAssert
실제 사용 시 제약이 많고, 복잡하다. @ScriptAssert를 억지로 사용하는 것보다 Object Error 관련 부분만 직접 자바 코드로 작성하는 것을 권장한다.
Bean Validation - groups
동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법
방법 2가지
- BeanValidation의 groups 기능을 사용한다.
- Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 모델 객체를 만들어서 사용한다.
BeanValidation groups 기능 사용
groups 적용
저장용 groups 생성
public interface SaveCheck {
}
수정용 groups 생성
public interface UpdateCheck {
}
Item - groups 적용
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
@NotNull(groups = UpdateCheck.class) //수정시에만 적용
private Long id;
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
private String itemName;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Range(min = 1000, max = 1000000, groups = {SaveCheck.class, UpdateCheck.class})
private Integer price;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Max(value = 9999, groups = SaveCheck.class) //등록시에만 적용
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
컨트롤러 수정
저장 컨트롤러에 SaveCheck Groups 적용
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//...
}
수정 컨트롤러에 UpdateCheck Groups 적용
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class)
@ModelAttribute Item item, BindingResult bindingResult) {
//...
}
참고 : @Valid에는 groups를 적용할 수 있는 기능이 없다. 따라서 groups를 사용하려면 @Validated를 사용해야 한다.
Form 전송 객체 분리
폼 데이터 전달에 Item 도메인 객체 사용
- HTML Form → Item → Controller → Item → Repository
- 장점 : Item 도메인 객체를 컨트롤러, 리포지토리까지 직접 전달해서 중간에 Item을 만드는 과정이 없어서 간단하다.
- 단점 : 간단한 경우에만 적용할 수 있다. 수정 시 검증이 중복될 수 있고, groups를 사용해야 한다.
폼 데이터 전달을 위한 별도으 객체 사용
- HTML Form → ItemSaveForm → Controller → Item 생성 → Repository
- 장점 : 전송하는 폼 데이터가 복잡해도, 그것에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다. 보통 등록과, 수정용으로 별도의 폼 객체를 만들기 때문에 검증이 중복되지 않는다.
- 단점 : 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가된다.
폼 데이터 전달을 위한 별도의 객체를 사용하고, 등록, 수정용 폼 객체를 나누면 등록, 수정이 완전히 분리되기 때문에 groups를 적용할 일은 드물다.
HTTP 메시지 컨버터
@Valid, @Validated는 HttpMessageConverter (@RequestBody )에도 적용할 수 있다.
@ModelAttribute vs @RequestBody
HTTP 요청 파라미터를 처리하는 @ModelAttribute는 각각의 필드 단위로 세밀하게 적용된다. 그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있었다.
HttpMessageConverter는 @ModelAttribute와 다르게 각각의 필드 단위로 적용되는 것이 아닌, 전체 객체 단위로 적용된다. 따라서 메시지 컨버터의 작동이 성공해서 ItemSaveForm 객체를 만들어야 @Valid, @Validated가 적용된다.
- @ModelAttribute는 필드 단위로 정교하게 바인딩이 적용된다. 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validato를 사용한 검증도 적용할 수 있다.
- @RequestBody는 HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고, 예외가 발생한다. 컨트롤러도 호출하지 않고, Validator도 적용할 수 없다.
'Spring > Spring MVC' 카테고리의 다른 글
스프링 MVC - 스프링 인터셉터 (2) | 2024.10.02 |
---|---|
스프링 MVC - 서블릿 필터 (1) | 2024.10.01 |
스프링 MVC - Validator 분리 (2) | 2024.10.01 |
스프링 MVC 오류 코드와 메시지 처리 (1) | 2024.10.01 |
스프링 MVC Validation - BindingResult, Field Error, Object Error (1) | 2024.10.01 |