Bean Validation
검증 기능을 매번 코드로 쓰는 것도 조금 번거롭습니다.
특정 필드의 검증 로직은 거의 빈 값이거나 특정 크기를 초과하는 것과 같은 매우 일반적인 로직이지만, 이러한 일반적인 로직을 표준화하는 것이 Bean Validation입니다.
이번 투고에서는 Bean Validation 표준 기술을 이용하여 어노테이션으로 검증 처리를 해 봅시다.
(엄밀히 말하면, Bean Validation 표준 기술의 구현인 하이바네이트를 사용할 예정)
설정
Bean Validation 종속성 추가 – build.gradle – dependencies
implementation 'org.springframework.boot:spring-boot-starter-validation'
하이버네이트가 지원하는 검증 주석
- @NotBlank, @NotNull, @Max, @Range 등 …
- 다음 링크에서 추가 주석을 더 볼 수 있습니다.
Bean Validation 적용
구현한 검증 로직을 사용하지 않고 Bean Validation 어노테이션을 적용하여 상품 관리 애플리케이션의 검증 처리를 해보자.
Bean Validation은 먼저 도메인에 적용해야 합니다.
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min=1000,max=1000000)
private Integer price;
@NotNull
@Max(value=9999)
private Integer quantity;
public Item() {}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- @NotBlank : 빈 공간, 공백 허용 X
- @NotNull : null을 허용하는 X
- @Range(min,max) : min~max 범위의 값이어야 합니다.
- @Max(value) : 최대 value 값까지만 허용.
이와 같이 검증 로직을 도메인에 걸면, Spring은 ValidationFactory로부터 Validator(검증기)를 자동적으로 생성해, 검증 대상을 직접 검증기에 넣어, 그 결과를 받습니다.
결과는 마찬가지로 BindingResult에 배치됩니다.
이제 컨트롤러를 수정해 봅시다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes)
{
마찬가지로 검증 대상 @ModelAttribute Item item에 @Validated 주석을 붙여넣어야 합니다.
(Validator를 사용하는 원리와 동일합니다.
)
검증 순서는 이렇게 흐릅니다.
먼저 @ModelAttribute의 각 필드에 형식 변환 시도합니다.
▶︎타입 변환 실패 시 typeMistmatch에서 FieldError 추가
▶︎타입 변환 성공 시 하이버네이트 어노테이션 기반 Validator를 적용하여 검증 프로세스를 거쳐 오류가 발생한 경우 FieldError 추가
(이 때 오류 코드는 주석 기반으로 생성됩니다)
ex) @NotNull의 경우, itemName에서 필드 에러가 발생했을 때에 NotNull.item.itemName, NotNull.itemName, NotNull.java.lang.String, NotNull
Bean Validation이 바인딩 실패(유형 변환 실패) 필드는 Bean Validation을 적용하지 않습니다.
바인딩에 실패하지 않으면 Bean Validation를 적용하는 것이 의미가 없기 때문에 아마 당연한 이야기.
Bean-Validation 검증도 메시지를 별도의 파일로 관리할 수 있습니다.
NotBlank={0} 공백X
Range={0} {2} ~ {1} 허용
Max={0}, 최대 {1}
{0}: 필드 이름이 주입됩니다.
{1}, {2}..: 어노테이션에 따라 다르므로 위의 링크를 참조하십시오.
메시지는 다음 우선순위로 기록됩니다.
- messageSource – 수동 병, 자동 병(메시지 파일)
- 주석의 message 속성 ex) @NotBlank(message = “필수 입력입니다”)
- 도서관 제공할 기본값 사용
Bean Validation – ObjectError 처리
FieldError는 어떻게 처리하는지 알았는데 ObjectObject는 Bean-Validation에서 어떻게 처리합니까?
Bean-Validation을 사용하여 ObjectError도 처리할 수 있습니다.
즉시 @ScriptAssert()를 사용하십시오.
@Data
@ScriptAssert(lang="javascript", script="_this.price * _this.quantity >= 10000", message="수량 * 개수 >= 10000 이어야 합니다.
")
public class Item {
그러나 실제로 사용해 보면 한계가 많습니다.
그 오브젝트의 범위를 넘었을 경우에는 해결할 수 없다든가… 등, 이러한 경우에는 대응이 어렵습니다.
그래서 추천하는 방법이 아닙니다.
ObjectError에 한해서는, 간단히 컨트롤러로 취급하는 것이 가장 메인터넌스하기 쉽습니다.
ObjectError는 컨트롤러에서 로직을 직접 구현하고 bindingresult에 넣는 방향을 지향합시다.
Bean Validation – 그룹화 (groups)
Bean Validation은 도메인을 직접 확인하는 형식으로 유연하게 설정할 수 없습니다.
즉, 상품 관리 시스템에 등록하는 경우 수량을 최대 10000개로 변경한 경우 수량을 최대 50000개로 설정하려는 경우에 문제가 발생합니다.
왜냐하면 등록이나 수정에서도 같은 도메인을 사용하기 때문입니다.
이러한 경우에 해결할 수 있는 방법이 그룹화입니다.
Bean validation의 groups 옵션을 사용하여 등록 검증, 수정 검증을 그룹으로 나누어 적용할 수 있습니다.
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Range(min=1000,max=1000000,groups = {SaveCheck.class, UpdateCheck.class})
private Integer price;
@Max(value=9999, groups = {SaveCheck.class})
private Integer quantity;
@Validated에서도 이와 같이 그룹을 설정하십시오.
참고: @Valid에는 groups 옵션을 적용할 수 없으므로 groups를 작성하려면 @Validated를 사용해야 합니다.
@PostMapping("/add")
public String addItem2(@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)
그룹을 사용하면 레벨이 클수록 복잡하고 읽기 어려워집니다.
그래서 실무에서는 groups를 잘 사용하지 않는다고 합니다.
전송 객체 분리 후 Bean Validation 적용
지금까지는 등록 또는 변경 또는 양식 데이터 전달에 Item 도메인 객체를 사용했습니다.
HTML 양식 ➣ Item ➣ Controller ➣ Item ➣ Repository 이러한 흐름에서 같은 방식으로 흐릅니다.
이 항목을 Item 도메인으로 나누고 유효성을 검사하면 유연하게 유효성 검사를 설정할 수 없으며 groups라는 번거로운 옵션을 사용해야 합니다.
실무에서 폼 데이터 전달을 위한 객체를 별도로 지정하여 사용합니다.
- 상품 관리를 예로 하면 등록 양식 개체(ItemSaveForm), 수정 양식 개체(ItemUpdateForm)처럼.
- 컨트롤러는 양식 객체의 데이터를 기반으로 Item 객체를 생성하고 사용하는 추가 프로세스를 제공하지만, 검증이 중복되지 않고 읽기 쉽습니다.
이 방법을 사용하는 것이 좋습니다. - 등록은 HTML 양식 ➣ ItemSaveForm ➣ Controller ➣ Item 만들기 ➣ Repository
- 수정은 HTML 양식 ➣ ItemUpdateForm ➣ Controller ➣ Item 만들기 ➣ Repository
양식 데이터를 전달하면 Item 도메인을 확인할 필요가 없습니다.
검증은 Form 오브젝트를 통해 별도 지정.
컨트롤러에도 적용했을 때의 등록의 경우
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult,
RedirectAttributes redirectAttributes)
{
@ModelAttribute에서 addForm에서 POST로 전달 된 item을 ItemSaveForm form에 필드 주입합니다.
1. @Validated 에서 ItemSaveForm 으로 검증된 필드만 주입됩니다.
유형 오류가 발생하면 bindingResult에 넣고 Bean-Validation을 적용합니다.
2. @ModelAttribute(“item”) 로 설정해야 합니다.
그렇지 않으면 Model에 itemSaveForm이라는 이름으로 추가되므로 뷰 템플릿에서 액세스하는 th:object도 itemSaveForm으로 설정해야 합니다.
if (form.getPrice() !
= null && form.getQuantity() !
= null)
{
int resultPrice = form.getPrice() * form.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object(){10000, resultPrice}, null);
}
}
ObjectError는 Item 도메인의 필드가 아닌 폼의 필드를 참조하도록 변경.
Item item = new Item(form.getItemName(),form.getPrice(),form.getQuantity());
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("status",true);
redirectAttributes.addAttribute("itemId",savedItem.getId());
리포지토리에는 항목을 기록해야 합니다.
따라서 form 객체의 필드를 기반으로 item을 만들고 넣었습니다.
수정의 경우도 같은 문맥이므로, 별도 프로세스를 설명하는 것은 생략하도록 합니다.
Bean Validation – @Validated @RequestBody
@Valid @Validated는 @ModelAttribute뿐만 아니라 @RequestBody (HttpMessageConverter)에도 적용 가능합니다.
@ModelAttribute는 HTTP 요청 매개변수(URL 쿼리 문자열 또는 POST 양식)를 처리할 때 사용됩니다.
@RequestBody를 사용하여 Http Body의 데이터를 객체로 변환할 때 사용합니다.
@ModelAttribute그러면 각 필드별로 세밀하게 적용되므로 특정 필드에 유형이 맞지 않는 오류가 발생해도 나머지 필드는 성공적으로 처리되고 객체가 생성되고 컨트롤러가 호출합니다.
- 검증 성공의 경우당연히 객체 생성 성공 및 컨트롤러 호출
- 유형 오류 발생 시 객체 생성 성공 (오류 발생 필드를 제외한 값은 주입 된 상태에서), 컨트롤러도 호출합니다.
- 필드 오류 발생 시에 객체를 만들고 컨트롤러를 호출합니다.
했습니다.
한편 @RequestBodyHttpMessageConverter 스테이지에서 JSON 데이터로 개체를 변경할 수 없으면 이후 단계 자체가 진행되지 않고 예외가 발생합니다.
즉, 컨트롤러도 호출되지 않고 Validator도 적용할 수 없습니다.
- 검증 성공의 경우당연히 객체 생성 성공 및 컨트롤러 호출
- 타입 에러 발생시에도 MessageConverter에서 개체를 만들지 못했습니다.
따라서 컨트롤러도 호출되지 않습니다. - 필드 오류 발생 시에 타입 에러는 패스한 상태이므로 MessageConverter를 통해 객체가 생성되었으므로 컨트롤러가 호출합니다.
되기 @Validator 적용그리고 필드 오류를 잡습니다.
Postman에서 테스트
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("/add")
public Object addItemRequestBody(@Validated @RequestBody ItemSaveForm form,
BindingResult bindingResult)
{
log.info("API 컨트롤러 호출");
if(bindingResult.hasErrors())
{
log.info("검증 오류 발생 errors={}", bindingResult);
log.info("{}",form);
return bindingResult.getAllErrors();
}
return form;
}
@PostMapping("/add2")
public Object addItemModelAttribute(@Validated @ModelAttirubte ItemSaveForm form,
BindingResult bindingResult)
{
log.info("API 컨트롤러 호출");
if(bindingResult.hasErrors())
{
log.info("검증 오류 발생 errors={}", bindingResult);
log.info("{}",form);
return bindingResult.getAllErrors();
}
return form;
}
}
<참고자료>
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard