요청 파라미터의 검증은 1차적으로 클라이언트 영역에서 하지만 데이터를 다루는 서버 영역에서도 반드시 체크가 필요하다. 스프링을 사용하면 보통은 Validator와 BeanValidator 두가지를 통해 검증한다. 두가지의 사용 용도는 경우의 따라 달라지기도 하지만 보통은 요청 파라미터만을 검증할 때는 BeanValidator를 적용하고, 파라미터를 이용해 계산을 하거나 별도의 조건이 있다면 Validator 를 사용한다.
BeanValidator
BeanValidator 는 데이터를 담는 객체의 필드마다 검증하고 싶은 애노테이션을 붙여 적용하는 방식이다. 애노테이션만 적용하면 되므로 if문을 사용해 코드가 지저분해지지도 않는다. BeanValidator 는 널을 허용하지 않는 것부터 숫자 제한, 이메일, 날짜 등 많은 검증을 지원한다.
더 자세한 내용은 아래 검증 애노테이션을 모아놓은 문서에서 확인할 수 있다.
사용방법
ItemSaveDto.java
@Data
public class ItemSaveDto {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
ItemUpdateDto.java
@Data
public class ItemUpdateDto {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
private Integer quantity;
}
만약 Item을 등록하고 수정하는데 사용되는 객체가 있을 때 BeanValidator 를 적용하려면 위처럼 객체를 분리하는 것이 좋다. 스프링에서 제공하는 @Vaildated 를 적용하면 group 으로 나누어 등록에 필요한 조건과 수정에 필요한 조건을 분리할 수도 있지만, 나중에 수정이 필요할 경우 더 복잡해질 수 있으므로 이렇게 구현하는게 베스트다.
ItemController.java
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "/addForm";
}
Item item = new Item();
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity());
//저장
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
return "redirect:/items/{itemId}";
}
사용 방법도 정말 간단하다. 스프링 부트를 사용하면 LocalValidatorFactoryBean 을 글로벌 Validator 로 등록한다. 이 Validator 는 @NotNull 같은 애노테이션을 보고 검증을 수행하며 개발자는 데이터를 담고 있는 객체 앞에 @Validated 만 붙여주면 된다.
주의할 점으로 직접 글로벌 Validator 를 직접 등록하면 스프링 부트는 BeanValidator 를 글로벌 Validator 로 등록하지 않고 애노테이션 기반의 빈 검증기가 동작하지 않으니 주의해야한다.
검증 순서
BeanValidator 는 검증하기 앞서 필드를 바인딩하여 데이터를 넣어주는 작업을 한다. 만약 Integer로 받는 필드에 String형의 데이터가 들어왔다면 바인딩 실패가 되고 BeanValidator 는 바인딩에 실패한 필드는 BeanValidation 을 적용하지 않는다.
만약 Integer로 선언된 price 필드에 String 값이 들어왔다면 에러 코드로 4가지 typeMismatch.item.price, typeMismatch.price, typeMismatch.java.lang.Integer, typeMismatch 를 갖게 된다.
바인딩은 성공했지만 검증 실패가 났을 경우에는 적용한 애너테이션을 기반으로 @Range에 대한 에러가 발생 했다면 4가지 Range.item.price, Range.price, Range.java.lang.Integer, Range 를 갖게 된다.
Validator 사용
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
//조건 검증
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
Validator 는 인터페이스로 상속받아 적용한다. Validator 를 적용하면 BeanValidator 처럼 필드를 검증할 수 있지만 필드 검증을 하기 위해 if절이 많아질수록 필드에 대한 검증인지 조건에 대한 검증인지인지 보기 어려워, 조건에 대한 검증을 할 때만 Validator 를 사용하는 것이 좋다.
DefaultMessageCodesResolver
DefaultMessageCodesResolver는 MessageCodesResolver 인터페이스를 상속받아 정의된 것으로 검증 실패한 필드나 조건에 따라 알맞게 오류 코드를 생성해준다.
객체 오류(조건에 대한 오류)
객체 오류의 경우 다음 순서로 2가지 생성
1 : code + "." + object name
2 : code
예) 오류 코드: required, object name: item
1 : required.item
2 : required
필드 오류
필드 오류의 경우 다음 순서로4가지 메시지 코드 생성
1 : code + "." + object name + "." + field
2 : code + "." + field
3 : code + "." + field type
4 : code
예) 오류 코드: typeMismatch, object name "user", field "age", field type: int
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch"
BeanValidation 메시지 찾는 순서
- 생성된 메시지 코드 순서대로 messageSource 에서 메시지 찾기
- 애노테이션의 message 속성 사용 @NotBlank(message = "공백! {0}")
- 라이브러리가 제공하는 기본 값 사용 공백일 수 없습니다.
errors.properties 적용
스프링 부트 사용 시 application.properties에 spring.messages.basename=messages,errors 적용