[Spring MVC] 검증2 - Bean Validation (P)
스프링과 통합
스프링 부트가 spring-boot-starter-validation
라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합한다.
글로벌 Validator
스프링 부트는 자동으로 LocalValidatorFactoryBean
을 글로벌 Validator로 등록하고, 이 Validator는 @NotNull
같은 애노테이션을 보고 검증을 수행. 이렇게 글로벌 Validator가 적용되어 있기 때문에, @Valid
, @Validated
만 적용하면 사용이 가능하다.
검증 오류가 발생하면 FieldError
, ObjectError
를 생성해서 BindingResult
에 담아준다.
검증 순서
@ModelAttribute
각각의 필드에 타입 변환 시도- 성공하면 다음으로.
- 실패하면
typeMismatch
로FieldError
추가
- Validator 적용
바인딩에 성공한 필드만 Bean Validation이 적용된다.
에러 코드
Bean Validation을 적용하고 bindingResult
에 등록된 검증 오류 코드를 보면 typeMismatch
와 유사하게 오류 코드가 애노테이션 이름으로 등록된다.
NotBlank
,Range
등 오류 코드를 기반으로 MessageCodesResolver
를 통해 다양한 메시지 코드가 생성된다.
@NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
메시지 등록
errors.properties
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
{0}
은 필드명이고, {1}
,{2}
…는 각 애노테이션 마다 다르다.
Bean Validation 메시지 찾는 순서
- 생성된 메시지 코드 순서대로
messageSource
에서 메시지 찾기 - 애노테이션의
message
속성 사용 ->@NotBlank(message = "공백! {0}")
- 라이브러리가 제공하는 기본 값 사용
오브젝트 오류
오브젝트 오류는 다음과 같이 @ScriptAssert()
를 사용해서 처리할 수 있다.
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
public class Item {
//...
}
메시지 코드
ScriptAssert.item
ScriptAssert
하지만 제약이 많고 복잡하며 검증 기능이 해당 객체의 범위를 넘어서는 경우도 종종 있는데, 이런 경우 대응이 어렵다.
따라서, 오브젝트 오류(글로벌 오류)의 경우 @ScriptAssert
를 억지로 사용하는 것 보다는 이 부분만 직접 자바 코드로 작성하는 것이 좋다.
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
코드에 적용
상품 수정에 Bean Validation을 적용.
Controller
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute Item item, BindingResult bindingResult) {
// 글로벌 오류 검증 코드
//...
}
Item 모델 객체에 @Validated
를 추가했다.
한계
데이터를 등록할 때와 수정할 때는 요구사항이 다를 수 있다.
요구 사항 변경
- 등록: 수량 최대 9999 제한 -> 수정: 제한X
- 등록:
id
에 값 X -> 수정:id
값 필수
Item.class
@NotNull //수정 요구사항 추가
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
//@Max(9999) //수정 요구사항 추가
private Integer quantity;
//...
id
:@NotNull
추가quantity
:@Max(9999)
제거
[참고]
구조상 수정시에는item
의id
값은 항상 들어가 있도록 로직이 구성되어 있지만, HTTP 요청은 언제든 악의적으로 변경해서 요청할 수 있으므로 서버에서 항상 검증해야 한다!
문제점
- 등록시
quantity
수량 제한 최대 값이 적용되지 않는다. - 등록시 화면이 넘어가지 않고 오류가 발생한다.
- 등록시에는
id
값이 없어@NotNull
을id
에 적용한 시점에서 검증에 실패하게 된다.
- 등록시에는
문제 해결 방법 2가지
- Bean Validation의 groups 기능을 사용
- Item을 직접 사용하지 않고,
ItemSaveForm
,ItemUpdateForm
같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용
groups
BeanValidation groups 기능 사용
등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적용할 수 있다.
groups 적용
저장용
public interface SaveCheck {
}
수정용
public interface UpdateCheck {
}
Item.class
@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;
// ...
}
Controller
// 등록 로직
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//...
}
// 수정 로직
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class)
@ModelAttribute Item item, BindingResult bindingResult) {
//...
}
[참고]
@Valid
에는 groups를 적용할 수 있는 기능이 없기 때문에 사용하려면@Validated
를 사용해야 한다.
정리
groups 기능을 사용해 등록과 수정시에 각각 다르게 검증할 수 있었지만, Item
은 물론이고, 전반적으로 복잡도가 올라갔다.
실무에서는 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용하기 때문에 groups 기능은 실제로 잘 사용되지 않는다.
<출처 : 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한)>
댓글남기기