Bean Validation
Controller단에서 Validation을 구현하는 것은, 소스코드도 복잡해지고 한 눈에 들어오기 어려워진다.
그래서 Spring에서는 Bean Validation
을 사용하도록 권장한다.
Bean Validation
은 JPA
처럼 추상 표준기술이다. 많은 구현체들이 존재하고, 대표적으로하이버네이트 Validation
이 있다.
Gradle 설정
implementation 'org.springframework.boot:spring-boot-starter-validation'
을 추가해줘야 한다.
jakarta.validation:jakarta.validation-api
그래들이 들어온 모습- 객체에 @Max, @NotNull 등을 사용할 때 있는 어노테이션이 여기 들어있다.
실 사용 모습
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() {
}
스프링을 제외한 Bean Validation Test Code 동작 원리
ValidatorFactory
,Validator
를 생성한다.
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Bean Validation
을 의도적으로 위반한 객체를 생성한다.
Item item = new Item();
item.setItemName(" ");//공백
item.setPrice(0); //가격은 0일 수 없다.
item.setQuantity(10000); //양은 9999까지
- validate를 시행하고, 결과를 출력한다.
Set<ConstraintViolation<Item>> violations = validator.validate(item);
for (ConstraintViolation<Item> violation : violations) {
System.out.println("violation = " + violation);
System.out.println("violation.getMessage() = " + violation.getMessage());
}
결과
BeanValidation
에도 원하는 ErrorCode 설정하기
BeanValidation
은 Annotation과 동일한 이름으로 등록된다.
ex) [NotNull.item.price,NotNull.price,NotNull.java.lang.Integer,NotNull]- 따라서 그냥 properties에 해당 이름을 따서 삽입해주면 된다.
BeanValidation
메시지 적용 순서
- 기본 생성 에러 코드 순서대로 errors.properties(허용된 properties)에서 찾는다.
ex) [NotNull.item.price,NotNull.price,NotNull.java.lang.Integer,NotNull]
- 어노테이션의
message
속성으로 찾는다. ->@NotBlank(message="공백 {0}")
- 라이브러리가 제공하는 기본 값 사용.
BeanValidation
의ObjectError
추가적으로@ScriptAssert
란 기능이 구현되어 있는데, 복잡하고 비효율적이다.FieldError
는 그냥 어노테이션으로 해결하고,ObjectError
는 기존 bindingResult.reject로 해결하면 된다.
BeanValidation
한계극복 (모든 CRUD 페이지가 같은 어노테이션이 적용된 객체를 공유할 경우)
동일한 객체로 수정과 추가를 사용할 경우, 각각 다른 검증 조건을 시행해야 할 경우
방법 2가지
groups
로 나눠서 분리하는 기능 사용- 별도 From별로 전송 객체를 생성하는 방법 사용
groups
로 나눠서 분리하는 기능 사용
공백 인터페이스를 생성해주고, 객체에 groups를 분리하여 설정해준다. 그리고 @Validated의 Value값으로 넣어주면
해당 컨트롤러가 실행될 때 분리해서 Validation을 시행한다.
- Interface 생성 (의미 없는 그냥 이름만)
public interface UpdateCheck {
}
public interface SaveCheck {
}
- 객체에 있는 validation Annotation에 groups 채워주기
@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 메서드의 @Validated의 Value 인자 채워주기 (* @Valid는 안된다)
- 아이템이 추가할 때는 Item 객체에 SaveCheck.class 그룹들만, 수정시에는 UpdateCheck.class그룹만 된다.
@PostMapping("/add")
public String addItemV6(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes){
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult){
}
- 별도 Form별로 전송 객체를 생성하는 방법 사용
등록, 수정 등 Form 별로 Java 객체를 생성해서 사용하는 방법이다.
- 객체 저장시에 쓰는 Form 생성
@Data
public class ItemSaveForm {
@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") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
}
}
- 객체 업데이트시에 쓰는 Form 생성
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
//수정에서는 수량은 자유롭게 변경 가능하다.
private Integer quantity;
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
}
수정과 생성 다른 조건으로 검증할 수 있게 했다. 보통 실무에서 사용하는 방법.
RestAPI 시에 사용되는 Validation
@ModelAttribute vs @RequestBody
@ModelAttribute
는 HTTP 요청 파라미터*(URL 쿼리 스트링, POST Form)을 다룰 떄 사용한다.@RequestBody
는 HTTP Body의 데이터를 객체로 변환할 때 사용(API JSON)
API 형변환 오류시 Controller 아예 작동 안하는 이유
@ModelAttribute
는 변수 단위로 작동하기 때문에, 타입 오류가 날 때, 나머지는 작동을 시킬 수 있다.
그런데 @RequestBody
붙은 컨트롤러에서는 JSON으로 통짜로 받아서 @ModelAttribute로 변환시켜야 하기 때문에,
그 사전에 HttpMessageConverter
에서 자바객체로 변환시에 오류가 나서 아예 Controller가 작동하지 않는다.
HttpMessageConverter
변환단계 이후,Validation
에서 오류 발생시 API Retrun 메세지
- Controller
@RestController //이후 모든 메서드에 @ResponseBody 붙여준다.(return 되는 값을 Json으로 변환해줌)
@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 Validation에 걸리게 잘못된 값 전송시
[
{
"codes": [
"Max.itemSaveForm.quantity",
"Max.quantity",
"Max.java.lang.Integer",
"Max"
],
"arguments": [
{
"codes": [
"itemSaveForm.quantity",
"quantity"
],
"arguments": null,
"defaultMessage": "quantity",
"code": "quantity"
},
9999
],
"defaultMessage": "must be less than or equal to 9999",
"objectName": "itemSaveForm",
"field": "quantity",
"rejectedValue": 999999,
"bindingFailure": false,
"code": "Max"
}
]
추후 @ResponseBody를 통해 BindingResult 결과값들을 반환한 이 결과값들을 수정해줄 것이다.