신규 요구사항: 검증 로직 추가
지금까지 만든 웹 애플리케이션은 폼 입력시 숫자를 문자로 작성하거나해서 검증 오류가 발생하면 오류 화면으로 바로 이동한다.
이렇게 되면 지금 우리가 만든 웹 서비스의 구조상, 사용자는 처음부터 해당 폼으로 다시 이동해서 입력을 해야 한다. 아마도 이런 서비스라면 사용자는 금방 떠나버릴 것이다. 웹 서비스는 폼 입력시 오류가 발생하면, 고객이 입력한 데이터를 유지한 상태로 어떤 오류가 발생했는지 친절하게 알려주어야 한다.
컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것이다. (그리고 정상 로직보다 이런 검증 로직을 잘 개발하는 것이 어쩌면 더 어려울 수 있다.)
참고
- 클라이언트 검증과 서버 검증
(검증은 크게 클라이언트에서 하는 검증과 서버에서 하는 검증이 있다. 클라이언트에서 하는 검증은 주로 자바스크립트 기반의 검증을 말하고, 서버에서 하는 검증은 HTTP 요청 데이터에 대해 서버에서 이뤄지는 뒷단의 검증을 말한다.)- 클라이언트만으로 검증하면, 조작할 수 있으므로 보안에 취약하다.
- 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
아무래도 클라이언트에서 하는 자바스크립트 검증같은 경우, 고객의 입력이 일어날 때 마다 실시간으로 반응해줄 수 있기 때문에, 고객이 빠른 피드백을 받을 수 있다. 반대로 서버에서는 서버에 데이터를 보내봐야 이게 정상적인지 아닌지 알 수 있기때문에 즉각적인 고객 사용성이 조금 부족할 수 있다.- 따라서 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수이다.
상품 저장 성공

상품 저장 검증 실패

상품 등록 검증 : 먼저 상품 등록 시 검증 코드를 작성해보자
ValidationItemControllerV1 = addItem
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);
}
}

검증 오류 보관
Map<String,String> errors = new HashMap<>();
검증 로직
StringUtils.hasText(item.getItemName()): item 객체의 getItemName() 메서드로부터 반환된 문자열이 비어 있지 않고, 실제로 텍스트를 포함하고 있는지를 확인한다.!StringUtils.hasText(item.getItemName()) 앞에 ! 가 붙어있으므로 문자열이 비어있다면~ 의 if문을 의미한다. item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000 : 가격이 null이거나 1000 미만이거나 1000000 초과인 경우 "price" 키로 오류 메시지를 맵에 추가한다.item.getQuantity() == null || item.getQuantity() > 9999 : 수량이 null이거나 9999 초과인 경우 "quantity" 키로 오류 메시지를 맵에 추가합니다.addForm.html 수정
1. css 추가: 오류 메시지 노출 시 보여줄 디자인 적용
.field-error{
border-color: #dc3545;
color: #dc3545;
}
2. 글로벌 오류 메시지 처리 : 특정 필드가 아닌 복합 룰 검증에 실패한 글로벌 오류 메시지를 처리해보자
<div th:if="${errors ?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
${errors ?.containsKey('globalError')} : errors 맵에 'globalError' 키가 존재하는지 여부를 검사한다.?.는 errors가 null인 경우 NullPointerException을 방지하기 위한 safe navigation 연산자를 의미한다. th:text="${errors['globalError']}": Thymeleaf의 th:text 속성을 사용하여 errors 맵에서 'globalError' 키에 해당하는 값을 출력한다. 3. 필드 오류 메시지 처리 - 상품명
<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>
4. 필드 오류 메시지 처리 - 가격
<input type="text" id="price" th:field="*{price}"
th:class="${errors?.containsKey('price')}? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="가격을 입력하세요">
<div th:class="field-error" th:if="${errors.containsKey('price')}" th:text="${errors['price']}">
가격 오류
</div>
5. 필드 오류 메시지 처리 - 수량
<input type="text" id="quantity" th:field="*{quantity}"
th:class="${errors?.containsKey('quantity')}? 'form-control field-error' : 'form-control'"
class="form-control" placeholder="수량을 입력하세요">
<div th:class="field-error" th:if="${errors.containsKey('quantity')}" th:text="${errors['quantity']}">
수량 오류
</div>
실행

정리
1. 만약 검증 오류가 발생하면 (입력 데이터를 유지한 상태로) 입력 폼을 다시 보여준다.
2. 검증 오류들을 고객에게 친절하게 안내해서 다시 입력할 수 있게 한다.
3. 검증 오류가 발생해도 고객이 입력한 데이터가 유지된다.
스프링이 제공해주는 검증 오류 처리 방법을 알아보자
핵심은 BindingResult이다.
BindingResult 추가

BindingResult bindingResult 파라미터의 위치는 @ModelAttribute Item item 바로 다음에 와야 한다.필드 오류 처리 - FieldError
//검증 로직
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 까지 허용합니다."));
}
FieldError 객체를 생성해서 bindingResult.addError에 담아두면된다.objectName : @ModelAttribute 이름field : 오류가 발생한 필드 이름defaultMessage : 오류 기본 메시지특정 필드가 아닌 복합 룰 검증 오류 처리(글로벌 오류) - ObjectError
if(item.getPrice() != null && item.getQuantity() != null){
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice < 10000){
bindingResult.addError(new ObjectError("item","가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
ObjectError 객체를 생성해서 bindingResult에 담아두면 된다. public ObjectError(String objectName, String defaultMessage) {}검증 실패 시 다시 입력 폼으로 이동
if(bindingResult.hasErrors()){
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
v2/addForm.html
<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>
<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 th: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 th:class="field-error" th:errors="*{quantity}">
수량 오류
</div>
</div>
타임리프 스프링 검증 오류 통합 기능
BindingResult를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공한다#fields : #fields로 BindingResult가 제공하는 검증 오류에 접근할 수 있다.th:errors : 해당 필드에 오류가 있는 경우, 태그를 출력한다.th:errorclass : th:field에서 지정한 필드에 오류가 있으면, class 정보를 추가한다. BindingResult
BindingResult가 있으면 @ModelAttribute 에 데이터 바인딩시 오류가 발생해도, 컨트롤러가 호출된다.@ModelAttribute에 바인딩 타입 오류?
BindingResult가 있으면, 그 문제에 대한 결과를 BindingResult에 담아둔다.--> 담아두고 컨트롤러를 정상 호출한다.BindingResult에 검증 오류를 적용하는 3가지 방법
1. ModelAttribute 의 객체에 타입 오류 등으로 바인딩이 실패하는 경우, 스프링이 FieldError 생성해서 BindingResult 에 넣어준다.
2. 개발자가 직접 넣어준다 -->bindingResult.addError(...)
3. Validator를 사용한다.
주의
- BindingResult 는 검증할 대상 바로 다음에 와야한다. 순서가 중요하다. (예를 들어서, @ModelAttribute Item item , 바로 다음에 BindingResult 가 와야 한다.)
- BindingResult 는 Model 에 자동으로 포함된다.
BindingResult와 Errors
사용자 입력 오류 메시지가 화면에 남도록 해보자
ValidationItemControllerV2
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));
}
}
실행 결과

FideldError 생성자
public FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage)
new FieldError("item", "itemName",item.getItemName(),false, null, null,"상품 이름은 필수 입니다.")
파라미터 목록
true인 경우, 에러가 바인딩 실패로 인한 것이며, false인 경우는 유효성 검사 실패로 인한 것 --> 간단히 말하면, 바인딩 실패는 사용자의 입력 데이터를 서버 객체로 매핑하는 도중에 발생하는 문제를 나타내며, 유효성 검사 실패는 바인딩이 성공했지만, 특정 규칙을 위반했을 때 발생하는 문제를 나타낸다.
오류 발생 시 사용자 입력 값 유지
new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다.")
rejectedValue 가 바로 오류 발생시 사용자 입력 값을 저장하는 필드다.bindingFailure은 클라이언트가 서버로 데이터를 전송할 때, 생긴 오류를 의미한다. 지금은 데이터를 전송한 뒤, 규칙을 위반 했으므로 false 을 적으면 된다.타임리프의 사용자 입력 값 유지
th:field="*{price}"th:field는 매우 똑똑하게 동작하는데, 정상 상황에는 모델 객체의 값을 사용하지만, 오류가 발생하면, FieldError에서 보관한 값을 사용해서 값을 출력한다. 스프링의 바인딩 오류 처리
타입 오류로 바인딩에 실패하면 스프링은 FieldError 를 생성하면서 사용자가 입력한 값을 넣어둔다.
그리고 해당 오류를 BindingResult 에 담아서 컨트롤러를 호출한다. 따라서 타입 오류 같은 바인딩 실패시에도 사용자의 오류 메시지를 정상 출력할 수 있다.
오류 메시지를 좀 더 체계적으로 다루어보자
FieldError 생성자
public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage)
지금까지는 FieldError 와 ObjectError 를 사용하면서, 오류 메시지를 defaultMessage 를 통해서 직접 작성해서 적용하였다.
--> 그런데 이런 오류 메시지도 한 군데서 일관성있게 관리하는 것이 더 좋다.
--> 그래서 FieldError 와 ObjectError는 생성자의 codes , arguments 파라미터를 제공하여 오류 메시지를 일관성있게 관리할 수 있도록 지원한다.
--> 이것을 통해서 이전에 메시지, 국제화에서 학습했던 것 처럼, 오류 메시지를 messages.properties 같은 곳에서 찾아온 다음, 없으면 defaultMessage 를 적용하는게 가능해진다.)
errors 메시지 파일 생성

스프링 부트 메시지 설정 추가를 해줌으로써, 해당 메시지 파일을 인식할 수 있게 설정을 추가한다. --> messages.properties , errors.properties 두 파일을 모두 인식한다.
errors.properties 파일추가

ValidationItemControllerV2 - addItemV3
//검증 로직
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,10000000},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));
}
}
codes는 String 배열로 넣어야 한다. argument는 Object 배열로 넣어야 한다. Object[]{1000, 1000000}와 같이 사용되며, 오류 메시지에서 {0}, {1}를 치환하기 위한 값을 전달한다. 컨트롤러에서 BindingResult 는 검증해야 할 객체인 target 바로 다음에 온다.
따라서, BindingResult 는 이미 본인이 검증해야 할 객체인 target 을 알고있다.
log.info("objectName={}",bindingResult.getObjectName());
log.info("target={}",bindingResult.getTarget());
로그를 찍은 후 실행해보니,
결론적으로 bindingResult 에서 target에 대한 정보를 가지고있는 것을 알수있다.
rejectValue(), reject()
BindingResult가 제공하는 rejectValue(), reject() 를 사용해서 깔끔하게 검증 오류를 작성해보자
addItemV4
//검증 로직
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);
}
}
rejectValue() : 필드 검증 오류 처리를 위해 사용한다. (참고로 첫 번째 파라미터로 바로 field가 나온다. BindingResult 가 이미 target을 알고있기 때문에 objectName은 입력하지 않는다.)reject() : 글로벌 검증 오류를 처리하기 위해 사용한다.
rejectValue()
bindingResult.rejectValue("price","range",new Object[]{1000, 1000000},null);
}
축약된 오류 메시지 코드
range.item.price를 모두 입력하지 않고, range만 입력했을 뿐인데, 오류 메시지를 잘 찾아서 출력을 도와준다.MessageCodesResolver를 이해해야 한다.오류 코드를 만들 때, 다음과 같이 자세하게 만들 수 있고
또느 다음과 같이 단순하게 만들 수도 있다.
단순하게 만들면, 범용성이 좋아서 여러 곳에서 사용할 수 있지만, 메시지를 세밀하게 작성하기는 어렵다
반대로 너무 자세하게 만들면, 범용성이 떨어진다.
가장 좋은 방법은 범용성으로 사용하다가, 세밀하게 작성해야 하는 경우에는 , 세밀한 내용이 적용되도록 메시지에 단계를 두는 것이다.
예를 들어, required라고 오류 코드를 사용한다고 가정해보자.
requird=필수 값 입니다 라는 메시지만 있으면 이 메시지를 선택해서 사용하는 것이다.
하지만 이렇게 required.item.itemName과 같이 객체명과 필드명을 조합한 세밀한 메시지 코드가 있으면 이 메시지를 높은 우선 순위로 사용하는 것이다.

--> 스프링은 MessageCodesResolver 라는 것으로 이러한 기능을 지원한다.
MessageCodesResolver에 대해서 알아보자
( MessageCodesResolver 의 개념을 이해하게 되면, 이전에 errorCode로 "required" 만 넣었음에도 불구하고, "required.item.itemName" 또한 오류 코드로 만들어진 배경을 이해할 수 있다. )
MessageCodesResolverTest
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
void messageCodesResolverObject() {
String[] messageCodes = codesResolver.resolveMessageCodes("required",
"item");
assertThat(messageCodes).containsExactly("required.item","required");
}
@Test
void messageCodesResolverField(){
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
for (String messageCode : messageCodes) {
System.out.println("messageCode = " + messageCode);
}
assertThat(messageCodes).containsExactly("required.item.itemName","required.itemName"
,"required.java.lang.String","required");
}
}
MessageCodesResolver
MessageCodesResolver는 인터페이스고, DefaultMessageCodesResolver 는 기본 구현체이다.ObjectError, FieldErrorDefaultMessageCodesResolver의 기본 메시지 생성 규칙
1. 객체 오류
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()는 내부에서 MessageCodesResolver를 사용한다.
FieldError
예시) rejectValue("itemName", "required")
다음 4가지 오류 코드를 자동으로 생성
required.item.itemName
required.itemName
required.java.lang.String
required
ObjectError
예시) reject("totalPriceMin")
totalPriceMin.item
totalPriceMin
오류 메시지 출력
타임리프 화면을 렌더링 할 때 th:errors 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다. (디폴트 메시지도 없는 경우 오류가 발생한다.)
오류 코드 관리 전략 --> 핵심은 구체적인것에서 덜 구체적인 것으로
MessageCodesResolver 는 required.item.itemName 처럼 구체적인 것을 먼저 만들어주고, required 처럼 덜 구체적인 것을 가장 나중에 만든다.왜 이렇게 복잡하게 사용할까?
required같은 메시지로 끝내고 정말 중요한 메시지는 꼭 필요할 때 구체적으로 적어서 사용하는 방식이 더 효과적이다. errors.properties

erroers.properties 를 보면, 크게 객체 오류와 필드 오류를 나누었고, 각각 범용성에 따라 레벨을 두었다.
예를 들어, itemName의 경우, required 검증 오류 메시지가 발생하면, 다음 코드 순서대로 메시지가 생성된다.
① required.item.itemName
② required.itemName
③ required.java.lang.String
④ required
그리고 이렇게 생성된 메시지 코드를 기반으로 순서대로 MessageSource에서 메시지를 찾는다.
구체적인 것에서 덜 구체적인 순서대로 찾는다(메시지에 1번(①)이 없으면 2번(②)을 찾고, 2번(②)이 없으면 3번(③)을 찾는다.)
이렇게 되면 만약에 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용 하면 된다.
정리
1. rejectValue() 호출
2. MessageResolver를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3. new FieldError()를 생성하면서, 메시지를 보관
4. th:errors에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출
스프링이 직접 만든 오류 메시지 처리
검증 오류 코드는 두가지로 나눌 수 있다.
가격에 영어 A를 입력해보자

BindingResult에 검증 오류 정보가 들어간다.BindingResult에 FieldError가 담겨있고, 메시지 코드가 생성된 것이다.rejecValue()를 호출하면서 오류 코드로 typeMismatch라고 넣어준 것이다.위 예시를 보면, 다음과 같이 4가지 메시지 코드가 입력되어 있다.
--> 스프링은 타입 오류가 발생하면 typeMismatch 라는 오류 코드를 사용한다.
errors.properties에 코드 추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
--> 레벨3, 레벨4 두가지만 추가해보자

컨트롤러에서 검증 로직이 차지하는 부분이 매우 크다
별도의 클래스로 역할을 분리하는 것이 좋다
ItemValidator 생성
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);
}
}
}
}
Validator 인터페이스를 제공한다addItemV5
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
itemValidator.validate(item,bindingResult);
//검증에 실패하면
if (bindingResult.hasErrors()) {
log.info("errors={}", 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}";
}
itemValidator.validate(item,bindingResult); 만 추가하면 사용 가능하다.스프링은 검증을 체계적으로 제공하기 위해 Validator를 제공한다
supports(Class<?> clazz) : 해당 검증기를 지원하는 여부 확인validate(Object target, Errors errors): 검증 대상 객체와 BindingResult --> 실제 검증하는 로직을 적용한다.스프링이 Validator 인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서이다.
WebDataBinder
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(itemValidator);
}
@InitBinder가 적용된 method가 호출된다.위 코드에서는 WebDataBinder에 검증기( itemValidator )를 항상 넣어 두고 있다. addItemV6
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증에 실패하면
if (bindingResult.hasErrors()) {
log.info("errors={}", 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}";
}
itemValidator.validate(item.bindingResult) 가 사라지고,@Validated가 붙었다.동작 방식
@Validated는 검증기를 실행시키는 애노테이션이다.supports()가 사용된다. @Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
supports(Item.class)가 호출이 되고, 결과가 true이기 떄문에, ItemValidator의 validate()가 호출이 된다.