[Spring Web MVC] Bean Validation
보통 작성하게 되는 검증 로직은 빈 값인지 확인하거나 값이 특정 크기를 넘는지 확인하는 등 복잡하지 않다.
지금까지는 간단한 로직을 작성하기 위해 코드를 좀 많이 써야 됐는데, Bean Validation을 잘 활용하면 애너테이션을 기반으로 검증을 정말 간단하게 처리할 수 있게 된다.
스프링이 제공하는 Bean Validation은 애너테이션과 인터페이스의 모음으로, 구현체는 적당히 골라 사용하면 된다.
그럼 Bean Validation을 어떻게 사용하는지, 그리고 어떻게 작동하는지 살펴보자.
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;
}
}
JPA를 사용한다면 @Entity 애너테이션을 붙이자.
의존관계를 추가하고 Bean Validation 애너테이션을 사용해보자.
Item 클래스를 바꿔봤는데, 딱 봐도 어떻게 동작하는지 직관적으로 알 것 같지 않은가?
여기서 클래스를 import 할 때 javax로 시작하면 특정 구현체에 관계없이 스프링이 기본적으로 제공하는 표준 인터페이스이고, org.hibernate로 시작하면 해당 구현체로 구현 할 때만 제공되는 기능이다.
@Test
void beanValidation() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setItemName(" ");
item.setPrice(0);
item.setQuantity(10000);
Set<ConstraintViolation<Item>> violations = validator.validate(item);
for (ConstraintViolation<Item> violation : violations) {
System.out.println("violation=" + violation);
System.out.println("violation.message=" + violation.getMessage());
}
}
검증기를 실행하려면 검증기를 만들고, 검증 대상에 값을 넣는다.
스프링은 이미 Bean Validation을 스프링에 완전히 통합해두었으니, 가져다 쓰기만 하면 된다. (따로 Validator를 작성하지 않아도 된다)
스프링은 애너테이션을 통해 검증 작업을 수행하는 LocalValidatorFactoryBean를 글로벌 Validator로 등록하고, 검증 오류가 발생하면 FieldError / ObjectError 를 생성해 BindingResult에 담아준다.
일단 @ModelAttribute 애너테이션으로 각각의 필드에 타입 변환을 시도하고, 바인딩에 성공한 요소들은 Validator를 적용, 실패한 요소들은 typeMismatch로 FieldError를 추가한다.
즉, 바인딩에 실패한 요소에 대해서는 BeanValidation을 적용하지 않는다.
Bean Validation의 에러 코드는 애너테이션 이름으로 등록되고 해당 이름을 기반으로 MessageCodesResolver가 메세지를 순서대로 만들어낸다.
순서대로 messageSource에서 메세지를 찾고, 없다면 애너테이션에 지정한 메세지를 찾고, 이래도 없으면 라이브러리가 기본으로 제공하는 값을 메세지로 사용한다.
특정 필드가 아니라 두 가지 이상의 필드를 한 번에 검증해야 하는 경우인 오브젝트 관련 오류는 @ScriptAssert 애너테이션을 사용해서 처리할 수 있다.
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "토탈 만원 이상으로 설정하세요")
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;
}
그런데.. 실제 사용해보면 제약이 많고 검증 대상이 객체의 범위를 넘어서는 등 한계가 많다.
오브젝트 관련 오류는 지난번에 하던 것 처럼 자바 코드로 작성하는 편이 합리적이다.
동일한 모델 객체를 각각 다르게 검증하는 방법을 알아보자.
예를 들어, 등록할 때와 수정할 때의 검증 방법이 다른 경우가 있다.
두 가지 방법을 사용한다.
1. BeanValidation의 groups 기능을 사용
2. Item 객체를 두 가지로 분리한다. (ItemSaveForm, ItemUpdateForm)
1. groups
저장용 / 수정용 group 인터페이스를 하나씩 만든다.
@NotNull(groups = UpdateCheck.class)
private Long id;
@NotBlank(groups = {UpdateCheck.class, SaveCheck.class})
private String itemName;
그 후 Item 클래스에서 해당 필드를 언제 검증할 지 만들어 놓은 group 인터페이스를 통해 명시해준다.
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item,BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//...
}
컨트롤러에서 검증을 처리하는 메서드에 해당 클래스를 넣어준다.
@Validated 애너테이션에서 그룹 클래스를 넣어주면 해당 그룹에 대해서만 검증 작업을 수행한다. (@Valid 애너테이션은 불가능)
2. Form 전송 객체 분리
사실 실무에서는 groups 기능을 잘 사용하지 않는다.
회원 등록 시 회원에 관련된 데이터 뿐만 아니라 약관 정보 등을 추가로 받는 등 회원과 관계 없는 여러 부가 데이터를 함께 입력받는 경우 때문인데, 이를 해결하기 위해 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 객체를 만들어서 전달한다.
ItemSaveFrom 객체를 만들어서 데이터를 전달한다고 생각해보자.
HTML 폼 -> ItemSaveForm -> Controller -> Item 생성 -> Repository
전송하는 폼 데이터가 복잡해도 폼에 맞춤형 객체를 사용해 데이터를 전달받는다.
폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 과정이 추가된다.
여기서 수정 시에는 ItemSaveForm 대신 ItemUpdateForm 객체를 적용해서 검증 로직을 쉽게 변경할 수 있다.
수정과 등록 작업에서 넘어오는 데이터가 전혀 다른 경우가 많기 때문에, 작업별로 처리하는 객체를 만들어서 처리하는 편이 합리적이고, 이렇게 처리하면 수정과 등록이 완전히 분리돼 groups 기능을 사용하지 않아도 된다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v4/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());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v4/items/{itemId}";
}
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
원래 Item 클래스에는 검증 관련 애너테이션을 모두 제거하고, ItemSaveForm / ItemUpdateForm 객체에 검증 관련 애너테이션을 추가한다.
ItemSaveForm 객체의 데이터를 바탕으로 Item을 만든 후 저장한다.
@ModelAttribute 애너테이션에 이름을 item으로 설정해 html파일을 그대로 유지했다.
@ModelAttribute 애너테이션은 쿼리나 form 의 POST 메서드 등 HTTP 요청 파라미터를 다룰 때 사용한다.
@RequestBody 애너테이션은 HTTP 바디 메세지를 객체로 변환해 API JSON 요청을 다룰 때 사용하는데,
여기서 @Validated 애너테이션을 사용할 수 있다.
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("/add")
public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) {
log.info("API 컨트롤러 호출");
if (bindingResult.hasErrors()) {
log.info("검증 오류 발생 errors={}", bindingResult);
return bindingResult.getAllErrors();
}
log.info("성공 로직 실행");
return form;
}
}
JSON 타입으로 데이터가 들어오면 해당 데이터를 ItemSaveForm 객체로 변환한다.
이 때 타입 오류로 인해 변환에 실패할 경우 Validator가 실행되지 않고 컨트롤러가 호출되지 않는다.
객체 변환에 성공했지만 검증에 걸린 경우 스프링은 ObjectError와 FieldError를 JSON으로 변환해 클라이언트에게 전달한다.
@RequestBody 애너테이션을 사용할 때 HttpMessageConverter는 각각의 필드 단위에 적용되지 않고 전체 객체 단위로 적용되기 때문에 타입 오류가 발생 시 다음 단계를 진행 할 수 없고, 예외가 발생한다.
몇 가지 애너테이션을 정리하고 가자.
@Valid
컨트롤러의 매개변수에서 객체의 유효성을 검사할 때 사용한다.
객체 내부의 필드에 설정된 검증 규칙을 검증한다 (@Size @Min 등..)
@Validated
@Valid처럼 유효성 검사를 수행하지만 추가로 그룹화된 유효성 검사를 지원한다.
수정할 때와 저장할 때 다른 유효성 규칙을 적용하고 싶으면 그룹화된 유효성 검사를 지원하는 @Validated 애너테이션을 사용하자.
@ModelAttribute
HTTP 요청 파라미터를 자바 객체에 바인딩할 때 사용한다. (쿼리스트링, Form)
주로 Form 데이터를 자바 객체로 변환할 때 사용한다.
해당 애너테이션과 @Valid 또는 @Validated 애너테이션을 함께 사용해 바인딩과 유효성 검사를 동시에 수행한다.
@RequestBody
역시 클라이언트가 전송한 데이터를 자바 객체로 변환하는 역할을 수행하지만, HTTP 메세지 본문에서 읽어들인다.
보통 JSON 데이터 타입의 메세지 본문을 변환한다.
'Spring > Spring Web MVC' 카테고리의 다른 글
[Spring Web MVC] API 예외 처리 (0) | 2022.08.26 |
---|---|
[Spring Web MVC] 예외 처리와 오류 페이지 (0) | 2022.08.26 |
[Spring Web MVC] 데이터 검증 (0) | 2022.08.22 |
[Spring Web MVC] 메세지와 국제화 (0) | 2022.08.20 |
[Spring Web MVC] Thymeleaf (2) (0) | 2022.08.20 |
댓글
이 글 공유하기
다른 글
-
[Spring Web MVC] API 예외 처리
[Spring Web MVC] API 예외 처리
2022.08.26 -
[Spring Web MVC] 예외 처리와 오류 페이지
[Spring Web MVC] 예외 처리와 오류 페이지
2022.08.26 -
[Spring Web MVC] 데이터 검증
[Spring Web MVC] 데이터 검증
2022.08.22 -
[Spring Web MVC] 메세지와 국제화
[Spring Web MVC] 메세지와 국제화
2022.08.20