검증이란 HTTP 요청이 정상인지 검증하는 것.
검증 로직을 처음부터 만들어보면서 어떻게 발전시킬 수 있는지 알아보자.
V1 - 직접 검증 개발하기
아래와 같이 코드를 통해 검증 로직을 직접 작성하는 방식의 검증 방법을 만들어보자.
에러들을 보관할 해쉬맵을 생성하고,
검증 로직을 실행하여 에러 발생 시 해쉬맵에 보관
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
//검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
errors.put("itemName", "상품 이름은 필수입니다.");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.put("price", "가격은 1,000 ~ 1,000,000 까지 허용합니다");
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
errors.put("quantity", "수량은 최대 9,999 까지 허용합니다.");
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격*수량의 합은 10,000 이상이어야 합니다. 현재 값 = " + resultPrice);
}
}
//검증에 실패하면 다시 입력 폼으로
if (!errors.isEmpty()) {
log.info("error = {}", errors);
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
}
이때 오류가 있다면 사용자에게도 이러한 오류를 보여주도록 만들어야 한다.
그러기 위해 errors는 모델에 넣어주고, 뷰템플릿에선는 다음과 같이 errors를 꺼내 사용한다.
사용법: $[errors['키 이름']}
기타 타임리프 문법에 대한 설명은 검증에 초점을 맞추기 위해 생략.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 th:text="#{page.addItem}">상품 등록</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${errors?.containsKey('globalError')}" >
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control' "
class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}">
상품명 오류
</div>
</div>
<div>
<label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price" th:field="*{price}"
th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control' "
class="form-control" placeholder="가격을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('price')}" th:text="${errors['price']}">
가격 오류
</div>
</div>
<div>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:field="*{quantity}"
th:class="${errors?.containsKey('quantity')} ? 'form-control field-error' : 'form-control' "
class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('quantity')}" th:text="${errors['quantity']}">
수량 오류
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/validation/v1/items}'|"
type="button" th:text="#{button.cancel}">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
버전 1에는 다음과 같은 문제점이 있음
- 뷰템플릿에서 중복 처리가 많다, 즉 좀 귀찮다.
- 타입 오류 처리가 안됨. 숫자 타입인 price에 문자타입이 입력되면 컨트롤러 자체가 실행되기도 전에 오류가 발생하기 때문에 검증이 되기도 전에 오류 페이지로 넘어간다. 이때 역시 사용자에게 타입을 잘못 입력했다고 알려줄 수 있어야 한다.
- 근데 만약 컨트롤러가 정상 호출된다고 해도, price에는 문자타입을 보관할 수 없는데, 어떤 방식으로 사용자에게 이를 알려줄 것인가가 문제다.
- 결론적으로 많은 한계점이 존재
V2 - 스프링이 제공하는 BindingResult 사용
우선 버전업된 코드를 확인하자.
//@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1,000,000 까지 허용합니다"));
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9,999 까지 허용합니다."));
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", "가격*수량의 합은 10,000 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("error = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
BindingResult : 오류의 내용을 보관해주는 스프링에서 제공해주는 객체
(** 반드시 @ModelAttribue 객체 뒤에 와야 한다. 타겟을 알아야 하기 때문에! **)
.addError()를 통해 에러를 보관해줄 수 있다.
이때 FieldError 객체와 ObjectError 객체를 생성해서 담는 것을 볼 수 있다.
FieldError : 특정 필드에 연관된 에러
FieldError의 생성자
public FieldError(String objectName, String field, String defaultMessage) {}
objectName : @ModelAttribue 이름
field : 오류가 발생한 필드의 이름
defaultMessage : 오류 기본 메시지
ObjectError : 특정 필드에 국한되지 않는, 글로벌 오류
ObjectError의 생성자
public ObjectError(String objectName, String defaultMessage) {}
objectName : @ModelAttribute 의 이름
defaultMessage : 오류 기본 메시지
아니 그래서, 뭐가 좋다는 건데? 뷰 템플릿을 살펴보자.
버전업된 뷰 템플릿:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 th:text="#{page.addItem}">상품 등록</h2>
</div>
<form action="item.html" th:action th:object="${item}" method="post">
<div th:if="${#fields.hasGlobalErrors()}" >
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
</div>
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">
상품명 오류
</div>
</div>
<div>
<label for="price" th:text="#{label.item.price}">가격</label>
<input type="text" id="price" th:field="*{price}"
th:errorclass="field-error" class="form-control" placeholder="가격을 입력하세요">
<div class="field-error" th:errors="*{price}">
가격 오류
</div>
</div>
<div>
<label for="quantity" th:text="#{label.item.quantity}">수량</label>
<input type="text" id="quantity" th:field="*{quantity}"
th:errorclass="field-error" class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:errors="*{quantity}">
수량 오류
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit" th:text="#{button.save}">상품 등록</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/validation/v2/items}'|"
type="button" th:text="#{button.cancel}">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
#fields : #fields 로 BindingResult 가 제공하는 검증 오류에 접근할 수 있다.
th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전이다.
th:errorclass : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.
결론 : v1에 비해 v2에서는 bindingResult 덕분에 뷰템플릿 코드가 훨씬 간결해졌다.
bindingResult의 장점은 이 뿐만이 아니다.
타입 오류 발생 시에도 오류 페이지를 띄우는 것이 아니라 오류 정보(FieldError)를 bindingResult에 담고 컨트롤러를 정상 호출한다.
근데 이때 오류가 발생하면 기존값이 다 지워진다.
FieldError와 ObjectError 생성시 다른 생성자를 사용하면 그 문제를 해결할 수 있다.
V2의 업그레이드1
개선코드는 아래와 같다.
//@PostMapping("/add")
public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다"));
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, null, null, "수량은 최대 9,999 까지 허용합니다."));
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", null, null, "가격*수량의 합은 10,000 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("error = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
FieldError와 ObjectError의 또 다른 생성자는 다음과 같다.
public FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage)
objectName : 오류가 발생한 객체 이름
field : 오류 필드
rejectedValue : 사용자가 입력한 값(거절된 값)
bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
codes : 메시지 코드
arguments : 메시지에서 사용하는 인자
defaultMessage : 기본 오류 메시지
ObjectError 역시 위와 유사한 생성자를 제공한다.
아무튼 위와 같은 생성자를 사용하면, 파라미터로 rejectedValue, 즉 사용자가 무엇을 입력했었는 지도 함께 저장해준다.
그럼 그 값을 이용하여 사용자의 입력값을 유지한다.
*그럼 타입 오류 발생 시에는?
타입 오류 발생 시에는 FieldError가 자동으로 생성되고, 그 잘못된 값을 저장해놓는다. 그리고 이를 bindingResult에 담은 뒤에 컨트롤러를 호출한다.
* 화면에 보여지는 방식을 좀 더 자세히!
th:field="*{price}"
타임리프의 th:field 는 매우 똑똑하게 동작하는데, 정상 상황에는 모델 객체의 값을 사용하지만, 해당 필드에 관련하여 오류가 존재한다면 FieldError의 rejectedValue에서 보관한 값을 사용해서 값을 출력한다.
근데 여기서 궁금점이 생길 수 있다.
인강을 통해 실행했던 예제에서 만약 price 입력안에 qq같은 문자 데이터를 입력하게 되면,
bindingResult에는 price 필드에 관한 오류가 총 2개 생길 것이다.
하나는 타입오류로 인해 스프링이 만들어준 FieldError이고
하나는 내가 작성한 로직에 위반하여 내가 만들어준 FieldError이다.
이때 각각의 FieldError에는 rejectedValue를 다르게 담고 있다. 하나는 qq, 하나는 null
그러면 타임 리프의 th:field는 오류 발생 시 둘 중에 무엇을 가져와서 화면에 보여주는가?
QNA를 참고하니 먼저 설정해준 FieldError의 값을 가져온다고 한다. 즉 여기선 타입 오류로 인한 에러가 먼저니까 화면에도 그 값이 뿌려진다.
결론적으로 코드 개선을 통해 내가 입력한 데이터가 모두 잘 유지되도록 하였다. 근데 여전히 개선점은 존재한다.
price에 관한 오류가 2개인 경우 그 2개에 대한 오류 메시지가 모두 화면에 출력된다.
V2의 업그레이드2
오류 메시지를 체계적으로 다뤄보자.
메시지는 지난 번에 배운대로 다음과 같이 설정을 추가할 수 있다.
//application.properties
spring.messages.basename=messages,errors
메시지 사용하도록 개선한 코드는 아래와 같다.
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]{9999},null));
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));
}
}
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("error = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
FieldError와 ObjectError의 파라미터인 codes, arguments에 적절한 값을 넣어주면 된다.
codes는 메시지 코드를 지정해주면 된다. 배열로 여러 값을 전달할 수 있고, 순서대로 매칭해서 먼저 매칭되는 메시지가 사용된다.
arguments에는 메시지 코드의 {0}, {1} 같은 변수를 지정해줄 수 있다.
V2의 업그레이드3
위의 코드를 좀 더 간략히 할 수 있다.
컨트롤러에서 BindingResult는 이미 검증 대상 객체인 target 바로 뒤에 오기 때문에
본인이 target해야 할 객체를 이미 알고 있다.
따라서 우리는 bindingResult가 제공하는 rejectValue(), reject() 등을 사용하여
직접 FieldError, ObjectError 등을 만들지 않도록 할 수 있다.
아래는 개선한 코드다.
//@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.rejectValue("price", "range",new Object[]{1000, 1000000}, null );
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
bindingResult.rejectValue("quantity", "max",new Object[]{9999},null);
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("error = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
이때 rejectValue()의 생성자는 다음과 같다.
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
그래서 얻는 장점은?
단순히 코드량을 줄일 수 있다는 장점 이외에도, errorCode를 사용할 때 편리해진다.
여기서 이 errorCode는 FieldError에 사용된 메시지 코드와는 약간 다르다.
위의 코드에서 보면
"required"라고만 코드를 넣어주었는데, 자동적으로 "required.item.itemName"이라는 메시지를 찾았다.
이게 가능한 이유는 MessageCodeResolver 때문이다.
MessageCodeResolver란?
- 검증 오류 코드로 메시지 코드들을 생성해준다.
- 기본 규칙은 다음과 같다.
1. 객체 오류의 경우
객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code
예) 오류 코드: required, object name: item
1.: required.item
2.: required
2. 필드 오류의 경우
필드 오류의 경우 다음 순서로 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"
- 동작 방식
rejectValue(), reject()를 사용하면 내부에서 이 리졸버를 사용하여 메시지 코드들을 자동 생성해준다.
이때 위와 같은 규칙들로, codes 부분에 배열 형태로 여러 메시지 코드들을 넣어준다.
그 예시로, rejectValue("itemName", "required")를 사용하면 아래와 같은 오류코드가 자동 생성된다.
- required.item.itemName
- required.itemName
- required.java.lang.String
- required
reject("totalPriceMin")의 경우,
- totalPriceMin.item
- totalPriceMin
그리고 출력시에는 타임리프가 th:errors가 실행될 때 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가며 메시지를 찾고, 만약 없으면 디폴트 메시지를 출력한다.
V2의 업그레이드4
- validator 클래스를 따로 만들어줌으로써 controller 내부의 코드를 눈에 보기 좋게 만들 수 있다.
package hello.itemservice.web.validation;
import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.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;
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
errors.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range",new Object[]{1000, 1000000}, null );
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
errors.rejectValue("quantity", "max",new Object[]{9999},null);
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
}
그럼 Controller는 아래와 같이 간단해진다.
private final ItemValidator itemValidator;
//@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
itemValidator.validate(item, bindingResult);
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("error = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
근데 왜 굳이 Validator라는 인터페이스를 구현하도록 ItemValidator를 만들었을까?
스프링의 추가적인 도움을 받을 수 있기 때문이다. 아래를 보자.
V2의 업그레이드5
- 컨트롤러에 다음을 추가하자. WebDataBinder에 검증기를 추가하면
해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있게 된다.
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
아래처럼 컨트롤러에선 더 이상 검증기를 따로 불러내지 않아도 된다.
대신 @Validated 어노테이션을 검증 객체에 달아준다.
//@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("error = {}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
@Validated 는 검증기를 실행하라는 애노테이션이다. 이 애노테이션이 붙으면 앞서 WebDataBinder 에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다.
----------------------------------------------
참고 : 인프런 김영한님 강의(스프링 MVC 2편 - 백엔드 웹 개발 활용 기술)
댓글