1. Bean Validation
이전 포스팅의 Validation 코드들을 보면, reject, rejectValue 등의 메소드를 활용해 검증을 수행했다. 하지만 이런 코드들은 너무 번거롭다. 하지만 특정 필드의 검증 로직은 대부분 빈 값인지, 특정 범위 안에 있는지 등 일반적인 것들을 목표로 한다. Bean Validation은 이러한 공통된 일반적인 검증 로직들을 @NotBlank, @Range, @Max 등의 어노테이션으로 제공한다.
- Bean Validation이란 검증 로직을 모든 프로젝트에 적용할 수 있도록 공통화/표준화한 것이다.
- 사실 Bean Validation은 기술 표준(인터페이스)으로 구현체가 따로 있는데, 주로 사용하는 구현체는 하이버네이트 Validator이다. (JPA라는 표준 기술의 구현체로 하이버네이트가 있는 것과 동일하다.)
Bean Validation을 사용하기 위해서는 다음과 같은 의존관계를 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
해당 의존관계가 추가되면 jakarta.validation의 javax에 Max, Min과 같은 여러 어노테이션이 들어있다. 이것의 구현체가 hibernate.validator이다. (즉, 인터페이스가 jakarta.validation api이고, 구현체는 hibernate.validator이다.) 아래와 같이 활용할 수 있다.
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 10000)
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;
}
}
검증기를 생성하려면 아래와 같이 코드를 작성하면 된다.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
그리고 생성한 item을 검증해보고 싶다면 아래와 같이 validator.validate() 메서드에 넣어주면 된다.
Set<ConstraintViolation<Item>> violations = validator.validate(item);
1.1. 스프링의 Bean Validation 통합
스프링은 이러한 Bean Validation을 스프링에 완전히 통합해놨고 우리는 가져다쓰면 된다. 위와 같이 검증 코드를 작성하고 아래와 같이 컨트롤러 코드를 작성하면 검증이 잘 동작된다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 검증 실패
if (bindingResult.hasErrors()) {
return "validation/v3/addForm";
}
// 검증 성공
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
- 'spring-boot-starter-validation'을 의존관계로 추가하면, 스프링 부트는 Bean Validator를 자동으로 인지하고 스프링에 통합한다. 스프링 부트는 이때 LocalValidatorFactoryBean을 Global Validator로 등록하며, 이 validator는 Bean Validator 어노테이션을 보면 검증을 수행한다. Global Validator가 등록되어 있으므로 @Valid 혹은 @Validated가 붙어있으면 적용된다. 검증 오류 발생시 FieldError, ObjectError를 생성항 BindingResult에 담아진다.
- @ModelAttribute로 바인딩을 수행할 때, typeMismatch 등의 오류로 바인딩에 실패할 수 있다. 이때 바인딩에 실패한 필드들은 Bean Validation을 적용하지 않는다. 사실 검증은 바인딩이 성공하고 적용해야 의미가 있기 때문이다.
1.2. Bean Validation 에러 코드
Bean Validation의 에러 코드들은 에노테이션 이름으로 등록된다. 예를 들어, Item 클래스의 itemName 필드에는 @NotBlank 에노테이션을 사용했는데 이때 생성되는 에러 코드는 다음과 같다.
- NotBlank.item.itemName, NotBlank.itemName, NotBlank.java.lang.String, NotBlank
즉, 만약 에러 코드를 변경하고 싶다면 해당 에러 코드들 중 하나를 에러 메시지들이 저장된 .properties파일에 등록하면된다. 예를 들어, 아래와 같은 에러 코드들을 추가할 수 있다.
NotBlank = {0} 공백 x
Range= {0}, {2}~{1} 허용
Max = {0}, 최대 {1}
- {0}은 필드의 이름이다. 즉, itemName, price, quantity가 전달된다.
만약 각 form에서 에러가 발생하면 다음과 같이 메시지가 전달된다.
1.3. Object 오류
오브젝트 오류의 경우 직접 자바 코드로 작성되는 것이 권장된다. 즉, 아래와 같이 Controller에 해당 로직을 작성하는 것이 권장된다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 오브젝트 오류 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("mulPriceQuantityMin", new Object[]{10000, resultPrice}, null);
}
}
...
}
1.4. Bean Validation의 한계 (groups)
이전까지 물품 추가 시 검증을 수행하는 코드를 작성했다. 그런데 물품 수정에서도 검증을 수정하되, 요구되는 검증이 다르면 어떻게 될까? 이런 경우 Bean Validation은 두 가지 이상의 요구 사항을 동시에 만족할 수 없다. 왜냐하면 Domain 코드에서 Bean Validation을 정의하기 때문에 한 가지 검증만 수행 가능하다. 물품 등록과 수정 시 각각 다르게 검증할 수 있는 방법은 2가지가 있다.
- BeanValidation의 groups 기능을 사용한다.
- Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm과 같이 폼 전송을 위한 별도의 모델 객체를 만든다.
groups 기능은 다음과 같이 적용할 수 있다. (먼저 SaveCheck, UpdateCheck라는 두 개의 인터페이스를 생성해야 한다.)
@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 = 10000, 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;
}
}
그러면 컨트롤러에서 상품 등록을 위한 코드에는 @Validated에 SaveCheck.class를 넘겨주면 되고, 상품 수정을 위한 코드에는 @Validated에 UpdateCheck.class를 넘기면 된다.
@PostMapping("/add")
public String addItem(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
...
}
- @Validated는 groups 기능을 제공하지만 @Valid는 groups 기능을 제공하지 않는다.
하지만 groups 기능은 잘 사용하지 않는다. 보통 실무에서는 request 객체들을 따로 만들어서 사용하기 때문에 주로 두 번째 방법을 사용한다. (즉, 위 예시를 기준으로 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용한다.)
2. Form 전송 객체 분리
먼저 상품 등록을 위한 요청 객체(ItemSaveRequest)는 다음과 같이 작성했다.
@Data
public class ItemSaveRequest {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 10000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
또한 상품 수정을 위한 요청 객체(ItemUpdateRequest)는 다음과 같이 작성했다.
@Data
public class ItemUpdateRequest {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 10000)
private Integer price;
@NotNull
private Integer quantity;
}
그럼 상품 등록 요청시 호출되는 컨트롤러 코드는 다음과 같이 변경된다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveRequest request, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// Object Error 검증 코드
...
// 검증 실패
if (bindingResult.hasErrors()) {
return "validation/v4/addForm";
}
// 검증 성공
Item item = new Item(
request.getItemName(),
request.getPrice(),
request.getQuantity()
);
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v4/items/{itemId}";
}
상품 수정에 대한 컨트롤러 코드도 동일한 방식으로 변경하면 된다.
3. HTTP Message Converter
Bean Validation은 @ModelAttribute뿐만 아니라 @RequestBody에도 사용할 수 있다.
- @ModelAttribute는 URL 쿼리 스트링, POST Form 등 HTTP 요청 파라미터를 처리할 때 사용한다.
- @RequestBody는 JSON 등 HTTP Body의 데이터를 객체로 변환할 때 사용한다.
먼저 아래와 같이 RestController를 작성했다.
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("add")
public Object addItem(@RequestBody @Valid ItemSaveRequest request, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return bindingResult.getAllErrors();
}
return request;
}
}
아래처럼 모든 에러가 발생하도록 JSON을 주었다.
그러면 아래와 같이 오류 관련 JSON 내용들이 반환된다.
단, typeError등의 이유로 검증에 실패할 경우 컨트롤러는 호출되지 않는다. 왜냐하면 ItemSaveRequest와 같은 요청 객체를 만들어야 뭐라도 하는데, 요청을 객체로 바꾸지도 못했기 때문에 아무것도 하지 못하기 때문이다.
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'BackEnd > Spring' 카테고리의 다른 글
[Spring MVC-2] 로그인 - 필터, 인터셉터 (0) | 2024.05.27 |
---|---|
[Spring MVC-2] 로그인 - 쿠키, 세션 (0) | 2024.05.27 |
[Spring MVC-2] Validation (0) | 2024.05.23 |
[Spring DB-1] 문제해결 - 예외 처리, 반복 (0) | 2024.05.08 |
[Spring DB-1] 문제 해결 - 트랜잭션 (0) | 2024.05.07 |