Bean Validation이란?
http://hibernate.org/validator/
The Bean Validation reference implementation. - Hibernate Validator
Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.
hibernate.org
- Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 자바에서 지원하는 기술 표준이다. 쉽게 이야기해서 검증 애노테이션과 여러 인터페이스의 모음이다.
- Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다. 이름이 하이버네이 트가 붙어서 그렇지 ORM과는 관련이 없다.
- Bean Validation을 잘 활용하면, 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.
- 검증 애노테이션 모음: https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec
Hibernate Validator 6.2.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide
Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th
docs.jboss.org
스프링 MVC는 어떻게 Bean Validator를 사용할까?
일단, 스프링 부트에서 Bean Validation을 사용하려면 해당 라이브러리 의존성을 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
- 해당 라이브러리를 넣으면 자동으로 스프링 부트가 Bean Validator를 인지하고 스프링에 통합한다.
스프링 부트는 자동으로 글로벌 Validator로 등록한다.
- 스프링 부트는 LocalValidatorFactoryBean을 글로벌 Validator로 등록하여 모든 컨트롤러에 이 Validator를 사용할 수 있도록 한다.
- LocalValidatorFactoryBean는 스프링 빈을 검증해주는 역할을 한다.
- (@NotNull, @Max, @Min ... 등) 검증 어노테이션을 보고 검증해주는 검증기
- 이렇게 글로벌 Validator가 적용되어 있기 때문에, 검증할 객체(@ModelAttribute) 앞에 @Valid , @Validated 만 적용하면 스프링MVC가 자동으로 검증기를 호출하여 검증한다.
- 검증 오류가 발생하면, FieldError , ObjectError를 생성해서 BindingResult 에 담아준다.
검증 순서
1. @ModelAttribute 객체 각각의 필드에 타입 변환 시도
- 성공하면 Validator 적용
- 실패하면 typeMismatch로 FieldError 추가
2. Validator 적용
- 객체 필드에 적용된 검증 어노테이션(@NotNull, @NotBlank, @Min, @Max ...)을 통해 BeanValidation 적용
* 바인딩에 성공한 필드만 Bean Validation 적용
BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다. 생각해보면 타입 변환에 성공해서 바인딩에 성공한 필드여야 BeanValidation 적용이 의미 있다.
즉, 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증도 의미가 있다.
@ModelAttribute -> 각각의 필드 타입 변환시도 -> 변환에 성공한 필드만 BeanValidation 적용
예시)
/* Item 클래스 */
public class Item {
@NotBlank
private String itemName; //상품명
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price; //상품가격
}
- itemName 에 문자 "A" 입력 -> 타입 변환 성공 -> itemName 필드에 BeanValidation 적용
- price 에 문자 "A" 입력 -> "A"를 숫자 타입 변환 시도 실패 -> typeMismatch FieldError 추가 -> price 필드는 BeanValidation 적용 X
에러코드
BeanValidation을 적용하고 BindingResult에 등록된 검증 오류코드를 보면, 오류코드가 객체 필드에 적용한 검증 어노테이션의 이름으로 등록된다. 마치 스프링이 타입 오류가 발생했을 때, 오류코드를 'typeMismatch'로 만든 것과 유사하다.
이처럼, 검증 어노테이션 이름을 기반으로 MessageCodesResolver를 통해 다양한 메시지 코드가 순서대로 생성된다.
@NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
따라서, 이 메시지 코드를 기반으로 오류 메시지 파일(errors.properties)에 설정을 해두면 Bean Validation의 오류메시지를 관리할 수 있다.
BeanValidation 메시지 찾는 순서
1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
=> 직접 생성한 오류 메시지 파일 (errors.properties)에서 찾기
[errors.properties]
#Bean Validation으로 검증이 이루어지면 오류코드가 검증어노테이션의 이름으로 등록된다.
#level 1
NotBlank.item.itemName=상품 이름을 적어주세요.
Range.item.price={0}는 {1} ~ {2} 까지 허용합니다.
Max.item.quantity={0}는 최대 {1} 까지 허용합니다.
#level 4
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
2. 검증 어노테이션의 messge 속성 사용
[Item 클래스]
@NotBlank(message = "공백 X") // errors.properties에 적용된 것이 없다면, 기본 오류 메시지로 출력됨.
private String itemName; //상품명
@NotNull(message = "null 허용 X")
@Range(min = 1000, max = 1000000)
private Integer price;
3. 라이브러리가 제공하는 기본값 사용
동일한 모델 객체를 기능(등록, 수정, ...)에 따라 다르게 검증하는 법
- BeanValidation - groups
- 모델 객체를 직접 사용하지 않고, 폼 전송을 위한 별도의 모델 객체를 생성하여 사용
만약, 상품을 등록할 때와 수정할 때 검증 요구사항이 각각 다르다면 어떻게 해결할 수 있을까?
groups
위와 같은 문제를 해결하기 위해 BeanValidtion은 groups라는 기능을 제공한다. groups는 말 그대로 검증을 적용하고 싶은 부분을 기능별 (등록, 수정 ...)로 그룹핑하여 적용한다.
groups 적용 예시
1. 먼저, 검증을 그룹핑하기 위해 기능별로 인터페이스를 생성한다.
[등록용 groups 생성]
package hello.itemservice.domain.item;
public interface SaveCheck {
}
[수정용 groups 생성]
package hello.itemservice.domain.item;
public interface UpdateCheck {
}
2. 모델 객체의 검증 어노테이션 속성에 'groups'를 통해 검증을 진행할 그룹을 추가한다.
[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;
...
3. @Validated에 그룹을 적용한다. (value 속성 적용)
[ItemController]
//상품 등록
//BeanValidation groups(SaveCheck) : 등록 검증만 적용.
@PostMapping("/add")
public String save(@Validated(value = SaveCheck.class) @ModelAttribute("item") Item item,
BindingResult bindingResult, RedirectAttributes redirectAttributes){
...
}
//상품 수정
//BeanValidation groups(UpdateCheck) : 수정 검증만 적용.
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable long itemId, @Validated(value = UpdateCheck.class) @ModelAttribute Item item,
BindingResult bindingResult){
...
}
[참고]
* @Valid 에는 groups를 적용할 수 있는 기능이 없다. 따라서 groups를 사용하려면 @Validated 를 사용해야 한다.
폼 전송 모델 객체 분리
실무에서는 groups를 잘 사용하지 않는데, 그 이유는 등록이나 수정 등의 폼에서 전달하는 데이터가 도메인 객체와 딱 맞아 떨어지지 않기 때문이다. 예를 들어, 회원 가입만 생각해도 회원 등록 시, 회원과 관련된 정보뿐 아니라 약관정보와 같이 부가적인 데이터들도 같이 넘어온다.
따라서, 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 모델 객체를 만들어서 전달한다. 그리고 그 객체를 가지고 실제 도메인 객체를 생성한다.
폼 데이터 전달에 Member 도메인 객체 사용
HTML Form -> Member (도메인 객체) -> Controller -> Repository
- 장점 : 도메인 객체를 controller, Repository에 직접 전달해서 중간에 새로 객체를 만드는 과정이 없어 간단하다.
- 단점 : 데이터가 간단한 경우에만 적용할 수 있다. 수정 시, 검증이 중복될 수 있고, groups를 사용해야 한다.
폼 데이터 전달을 위한 별도의 객체 사용
HTML Form -> MemberSaveDto -> Controller -> Member 생성 -> Repository
- 장점 : 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있다.
보통 등록과, 수정용으로 별도의 폼 객체를 만들기 때문에 검증이 중복되지 않는다. - 단점 : 폼 데이터를 기반으로 컨트롤러에서 도메인 모델 객체를 생성하는 변환 과정이 추가된다.
사용 예시)
[ 도메인 모델 - Item ]
@Data
public class Item {
private Long id;
private String itemName
private Integer price;
private Integer quantity;
private Boolean open; // 판매 여부
private List<String> regions; // 등록 지역
private ItemType itemType; // 상품 종류
private String deliveryCode; // 배송 방식
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- 보다시피 검증을 도메인 모델 객체에서 더 이상 하지 않는다.
[ 등록 폼 전송 모델 객체 - ItemSaveDto ]
package hello.itemservice.web.validation.form;
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 ItemSaveDto {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
- 등록 폼으로부터 받은 데이터를 해당 객체로 받아 검증 어노테이션을 통해 검증을 진행한다.
[ 컨트롤러 ]
...
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveDto itemDto, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
...
//Dto를 가지고 item 객체 생성 (도메인 객체를 생성하는 변환 과정)
Item item = new Item();
item.setItemName(itemDto.getItemName());
item.setPrice(itemDto.getPrice());
item.setQuantity(itemDto.getQuantity());
//item객체 등록
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v4/items/{itemId}";
}
...
- 주의 : @ModelAttribute("item") 에 item 이름을 넣어준 부분을 주의
- 이것을 넣지 않으면 ItemSaveForm 의 경우 규칙에 의해 'itemSaveForm'이라는 이름으로 MVC Model에 담기게 된다.
- 위와 같이 중간에 폼 전송 모델 객체가 추가되면 도메인 객체를 생성하는 변환 과정이 추가된다.
'Spring > SpringMVC' 카테고리의 다른 글
필터, 인터셉터 (0) | 2024.02.11 |
---|---|
쿠키, 세션을 활용한 로그인, 로그아웃 (0) | 2024.02.04 |
검증 - Validation 2 (1) | 2024.01.21 |
검증 - Validation (1) | 2024.01.15 |
메시지와 국제화 (1) | 2024.01.11 |