김영한 님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
타입, 필드, 범위 등 여러 가지에 대해 검증할 수 있다
검증 오류가 발생했을 때 오류 화면으로 이동하면 사용자가 입력한 데이터가 사라져 처음부터 다시 작성해야하는데 이런 방식은 좋지 않은 방식
입력 데이터를 유지한 상태로 어떤 오류가 발생했는지 알려주는 것이 좋은 방식
컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것이다
클라이언트 검증, 서버 검증
클라이언트 검증은 주로 자바스크립트로 하는 검증, 조작이 가능해서 보안에 취약
서버 검증은 HTTP 요청 데이터가 서버로 넘어와서 컨트롤러나 다른 로직을 활용해서 검증하는 방식, 즉각적인 고객 사용성이 부족
클라이언트와 서버를 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
API 방식을 사용하는 경우, API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 한다
- /add에 GET 방식으로 상품 등록 폼을 요청
➜ Controller가 상품 등록 폼을 부르면 HTML로 렌더링돼서 웹 브라우저에 전달
- 데이터를 입력 후 저장 버튼을 누르면 /add에 POST 방식으로 요청
➜ Controller에서 폼으로 넘어온 데이터를 저장하고 상품 상세로 리다이렉트
사용자가 상품 등록 폼에서 정상 범위의 데이터를 입력하면, 서버에서는 검증 로직이 통과
사용자가 상품명을 입력하지 않거나, 가격, 수량 등이 너무 작거나 커서 검증 범위를 넘어서면, 서버 검증 로직이 실패
검증에 실패한 경우 고객이 입력한 정보를 유지하면서 다시 상품 등록 폼을 보여주고, 어떤 값을 잘못 입력했는지 알려주어야 한다
// 검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
// 검증 로직
// ModelAttribute를 통해 넘어온 item에 itemName이 없는 경우
if (!StringUtils.hasText(item.getItemName())) {
errors.put("itemName", "상품 이름은 필수입니다.");
}
// 특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
}
}
어떤 오류인지 담아주는 객체가 필요 ➜ Map 객체 생성
POST 방식으로 넘어와 @ModelAttribute
에 담긴 데이터를 보고 Map 객체에 오류와 오류 메세지를 저장한다
// 검증에 실패하면 다시 입력 폼으로
if (!errors.isEmpty()) {
log.info("errors = {}", errors);
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
모든 오류를 담고 난 후, 오류가 있는 경우 Map 객체에 담긴 오류 내용을 Model에 담고 상품 등록 폼을 다시 반환한다
기존에 작성되어 있던 로직 ( 상품 저장 및 상품 상세로 리다이렉트 )은 검증이 성공헀을 때만 수행되게 된다
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "validation/v1/addForm";
}
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {
...
}
<!-- Thymeleaf ( addForm.html ) -->
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
</div>
</form>
GetMapping을 처리할 때 Model에 새로운 Item 객체를 만들어서 넘겨줬는데 사용자가 특정 값을 입력하면 Item 객체( item )에 데이터가 담긴다
데이터를 저장하면 PostMapping을 처리하는 메서드의 @ModelAttribute
를 통해 item에 데이터가 들어오고 model에 자동으로 item이 들어간다
model.addAttribute("item", item);
이 수행된다그렇기 때문에 오류가 발생한 경우, 오류 메세지에 대한 처리만 하고 상품 등록 폼을 반환하면 model에서 기존에 입력된 데이터를 꺼내서 화면에 표시해준다
BUT> 타입이 다른 경우 Controller에 진입하기 전에 400 예외가 발생하면서 오류 페이지로 넘어가게 된다
<!-- Thymeleaf ( addForm.html ) -->
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메세지</p>
</div>
<!-- 페이지 소스 보기 -->
<div>
<p class="field-error">가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = 100</p>
</div>
errors?
errors 가 null 일때 NullPointerException
이 발생하는 대신 null 을 반환
처음 상품 등록 폼에 들어갔을 때는 errors 자체가 없기 때문에 null이 반환되고 th:if
에서 null 은 실패로 처리되므로 오류 메시지가 출력되지 않는다
th:if="${errors?.containsKey('globalError')}"
: errors 객체에 globalError라는 key가 있는 경우
th:text="${errors['globalError']}"
: 프로퍼티 접근법으로 errors 객체에서 globalError에 해당하는 Value를 꺼낸다
필드 오류도 위와 동일한 방식으로 처리한다
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
...
}
BindingResult는 검증 오류를 보관하는 객체이다
@ModelAttribute
다음에 위치하여 ModelAttribute의 객체에 바인딩된다
public interface BindingResult extends Errors {
void addError(ObjectError error);
}
addError()
를 통해 BindingResult 객체에 FieldError 과 ObjectError 추가한다
addError()
는 ObjectError 를 파라미터로 받는데 FieldError 가 들어갈 수 있는 이유는 FieldError 가 ObjectError 를 상속받았기 때문이다
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
필드 단위의 에러는 스프링이 제공하는 FieldError 객체에 넣는다
파라미터는 오브젝트 이름, 필드명, 기본 오류 메세지인데 오브젝트 이름에는 @ModelAttribute
로 지정한 이름을 넣는다
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("bindingResult = {}", bindingResult);
return "validation/v2/addForm";
}
bindingResult에 에러가 담긴 경우 실행되는 코드 ( bindingResult.hasErrors()
)
BindingResult는 저장된 내용을 모델에 담지 않아도 자동으로 View에 같이 넘어간다
<!-- Thymeleaf ( addForm.html ) -->
<!-- 글로벌 오류 처리 -->
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err: ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메세지</p>
</div>
<!-- 필드 오류 처리 -->
<div class="field-error" th:errors="*{itemName}">
상품명 오류
</div>
<!-- 오류 발생 시 CSS 처리 -->
<input type="text" id="itemName" th:field="*{itemName}"
th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
#fields
: #fields
로 bindingResult가 제공하는 검증 오류에 접근 가능th:each="err: ${#fields.globalErrors()}"
globalErrors()
로 반환되는 객체가 컬렉션 객체이기 때문에 th:each
로 반복한다
th:text="${err}"
로 오류 메세지를 출력한다
th:errors="*{필드명}"
지정된 필드에 오류가 있는 경우 태그를 출력한다
위의 예시의 경우 bindingResult에 itemName이라는 필드명을 가진 FieldError가 있는 경우 해당 태그가 출력된다
th:field="*{필드명}" th:errorclass="field-error"
th:field
에 지정된 필드명으로 된 오류가 있는 경우, th:errorclass
가 "field-error"라는 class 정보를 해당 태그에 추가해준다Item 객체에는 itemName, price 등의 필드가 있다
BindingResult
객체에 FieldError 혹은 ObjectError를 생성하면서 Error를 추가해준다
FieldError는 오브젝트 이름, 필드명, 기본 에러 메세지를 파라미터로 받는다
@ModelAttribute
를 통해 넘어온 이름을 의미하고, 필드명은 아마 넘어온 오브젝트의 필드명을 의미하는 것 같다BindingResult는 따로 View에 넘겨주지 않아도 자동으로 넘어간다 & 타임리프에서 사용할 수 있도록 #fields
나 th:errors
와 같은 것들이 제공된다
th:object
와 th:field
에 들어가는 값들은 @ModelAttrribute
가 item 객체를 자동으로 model에 담아 넘겨주기 때문에 오류가 발생해서 페이지를 다시 보내는 경우, 자동으로 정보가 유지된다
자동 처리 : @ModelAttribute
의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 스프링이 자동으로 FieldError를 생성해서 BindingResult 에 넣어준다
수동 처리 : 개발자가 검증 로직을 구성하여 직접 FieldError이나 ObjectError를 생성해서 넣어주는 방법
Validator
를 사용하는 방법
Q1.
2번에서 하는 방식은 타입이 다른 경우 Controller에 진입하기 전에 400 예외가 발생하면서 오류 페이지로 넘어가는 문제가 존재ex> Item의 price는 Integer 타입인데 문자를 작성하고 저장버튼을 누르는 경우 Controller 호출 안됨 + 사용자가 입력한 값 사라짐
@ModelAttribute
에 데이터 바인딩 시 타입 오류가 발생하는 경우, Controller가 호출되지 않고 오류 페이지로 넘어가는 문제를 해결하기 위해서 사용
BindingResult가 없으면 400 오류가 발생하면서 Controller가 호출되지 않고, 오류 페이지로 이동한다고 하였다
BindingResult를 사용하면 바인딩이 실패하는 경우 오류 정보( FieldError )를 BindingResult에 담아서 Controller를 정상 호출하기 때문에 이런 문제를 해결할 수 있다
Q2.
BUT> BindingResult를 사용하면 2번과 다르게 오류 발생 시 사용자가 입력한 값이 사라지는 문제가 존재한다그렇다면 왜 2번에서는 유지되던 값이 갑자기 3번에서 유지되지 않았는가?
th:field
는 오류가 발생하면 FieldError에서 보관한 값을 사용해서 값을 출력한다
하지만 2번에서는 BindingResult와 FieldError를 사용하지 않았기 때문에 model에 담긴 데이터가 그대로 출력된 것이고
3번의 경우 BindingResult와 FieldError를 사용했지만 FieldError 객체 생성 시, rejectedValue가 null인 생성자를 사용했기 때문에 사용자의 값이 유지되지 않아 사라지게 되는 것이다
FieldError와 rejectedValue에 관한 내용은 아래에 있는 4번에 작성해두었다
Q3.
바인딩 시 타입 오류가 발생했을 때 BindingResult와 FieldError가 동작하는 과정은 어떻게 되는가?
타입 오류가 발생해 @ModelAttribute
에 바인딩되는 시점에 오류가 발생하면 모델 객체에 사용자 입력 값을 유지하기 어려운데 이와 같은 상황에서도 사용자가 입력한 값을 유지할 수 있도록 BindingResult와 FieldError 객체를 사용한다
과정을 살펴보면 타입 오류가 발생하면 스프링이 자동으로 FieldError를 생성해서 BindingResult 에 넣어준다
바로 이 때, FieldError의 rejectedValue에 사용자가 입력했던 값을 넣어주기 때문에 바인딩 시점에 타입 오류가 발생해도 사용자가 입력한 값을 유지할 수 있는 것이다
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수입니다."));
}
rejectedValue
거절된 값 ( 사용자가 입력한 값 )
위에서 사용자가 입력한 값이 사라지는 문제가 존재한다고 했는데 bindingResult의 rejectedValue에 item 객체의 값을 추출하여 넣어준다
이렇게하면 오류가 발생했을 때 사용자가 입력한 값이 사라지는 문제를 해결할 수 있다
bindingFailure
타입오류와 같은 바인딩 실패인지, 검증 실패인지를 구분하는 구분 값 ( 바인딩이 실패했는지에 대한 여부 )
값은 잘 넘어왔기 때문에 false로 설정
code
: 메세지 코드
arguments
: 메세지에서 사용하는 인자
위의 코드를 보면 3 번째 파라미터에 rejectedValue 가 들어가는데 여기에 item.getItemName()
을 전달했다
타입 오류가 발생한다면 item 에 제대로 된 값이 저장되지 않을텐데 실행시켜보면 입력했던 문자가 그대로 출력된다
이게 어떻게 가능한 것인지 궁금해서 Q & A 를 찾아보았는데 내가 찾은 정답은 아래와 같다
오류가 없으면 Model의 값을 가져오고, 오류가 있다면 BindingResult 안에서 값을 가져오게 된다
이 때, BindingResult 에 해당 정보들이 문자로 남아있고, 해당 정보를 찾아서 뿌려주게 되는 것이다
FieldError 객체를 생성할 때 하나씩 메세지를 작성해주었는데 이렇게 하지 않고 한 곳에서 메세지를 관리하는 것이 좋은 방법이다
FieldError의 codes
와 argument
라는 파라미터를 통해 메세지를 관리하는 파일( XXX.properties )에서 찾아와 일치하는게 없으면 기본 디폴트 오류 메세지를 출력한다
즉, codes
와 argument
는 오류 발생 시 오류 코드로 메세지를 찾기 위해 사용된다
메세지 파일을 별도로 만들어 사용하려면 application.properties에 아래와 같이 입력해야한다
spring.messages.basename=messages, errors
기본으로 사용되는 messages, 오류 메세지를 관리하는 errors
< errors.properties >
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
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));
}
codes에는 찾지 못했을 경우, 여러 개를 찾을 수 있도록 String[] 로 넘겨준다 ( 하나인 경우에도 String[] 로 넘겨주어야 한다 )
FieldError의 argument에 Object[]로 값을 넘겨주면 메세지 파일의 argument에 값이 들어간다
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
}
// 결과
objectName=item
target=Item(id=null, itemName=test, price=10000, quantity=2)
BindingResult 는 검증해야 할 객체인 target 바로 다음에 오기 때문에 ( @ModelAttribute
바로 뒤에 오기 때문에 ) 검증해야할 객체가 무엇인지 알고 있다
➜ target(item)에 대한 정보는 없어도 된다
로그로 확인해보면 target 에는 객체 자체가 들어있는 것을 확인할 수 있다
즉, objectName 을 알고 있는 것이기 때문에 FieldError 나 ObjectError 를 생성하면서 objectName 을 넘겨주지 않아도 된다
더 나아가서 rejectValue()
나 reject()
를 활용하면 FieldError 나 ObjectError 를 직접 생성하지 않아도 된다
rejectValue()
, reject()
// Errors 인터페이스
// Register a field error for the specified field of the current object, using the given error description.
void rejectValue(@Nullable String field, String errorCode);
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
// Register a global error for the entire target object, using the given error description.
void reject(String errorCode);
void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
field
: 오류 필드명
errorCode
: 오류 코드
메시지 파일에 등록된 코드가 아님
messageResolver
가 메세지 코드를 생성할 수 있도록 하기 위한 오류 코드
위에서 넘겨준 errorCode 를 통해 messageResolver
가 메세지 코드를 생성한다
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.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
메서드에 넘겨준 errorCode 로 에러 메세지를 찾는 규칙이 존재한다
결론적으로 보자면 errorCode + objectName + 필드명 을 조합해서 오류 메세지가 있는 곳에서 코드를 찾는다
뒤의 MessageCodesResolver
에서 자세히 설명
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}
// 위의 코드를 이렇게 한 줄로 적을 수 있다
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
모든 메세지를 자세히 만들 수도 있고, 단순하게 만들 수도 있다
자세한 버전 : required.item.itemName
단순한 버전 : required
단순하게 만들면 범용성이 좋아 여러 곳에서 사용 가능, 자세히 만들면 사용할 수 있는 곳이 제한
가장 좋은 방법은 범용성으로 사용하다가 세밀하게 작성해야 하는 경우에 세밀한 내용이 적용되도록 메세지에 단계를 두는 것이다
예를 들어, errorCode에는 동일하게 계속 required를 사용하는데 메세지 파일에 required와 required.객체명.필드명인 메세지가 있다고 가정했을 때 자세히 작성한 메세지를 높은 우선순위로 사용하는 것이다
이처럼 개발을 하면 자바 코드를 변경하지 않고 메세지 파일만 수정하면 전체 메세지를 관리할 수 있다
➡️ 스프링이 MessageCodesResolver라는 것으로 위와 같은 기능을 지원한다
public interface MessageCodesResolver {
// Build message codes for the given error code and object name. Used for building the codes list of an ObjectError.
String[] resolveMessageCodes(String errorCode, String objectName);
// Build message codes for the given error code and field specification. Used for building the codes list of an FieldError.
String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class<?> fieldType);
}
resolveMessageCodes()
: errorCode를 받으면 여러 개의 메세지 코드를 반환해준다
인터페이스의 기본 구현체는 DefaultMessageCodesResolver
@Test
void messageCodesResolverObject() {
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
for (String messageCode : messageCodes) {
System.out.println("messageCode = " + messageCode);
}
assertThat(messageCodes).containsExactly("required.item", "required");
}
// 테스트 결과
messageCode = required.item
messageCode = required
BindingResult.reject()
가 내부적으로 MessageCodesResolver
를 사용해 messageCodes를 얻는다resolveMessageCodes()
가 반환한 값들을 가지고, BindingResult.reject()
가 아래의 코드를 실행시키는 것이다
new ObjectError("item", new String[]{"required.item", "required"});
ObjectError 생성자는 여러 개의 오류 코드를 가질 수 있다
객체 오류의 경우, 기본 메세지 생성 규칙은 아래와 같다 ( 구체적인 것을 먼저 만든다 )
errorCode.오브젝트이름
errorCode
오류가 발생하면 타임리프 화면을 렌더링할 때 th:errors
가 실행되는데 th:errors
가 생성된 오류 메세지 코드를 순서대로 돌아가면서 메세지를 찾게 되고, 찾지 못하면 디폴트 메세지를 출력한다
@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");
}
// 테스트 결과
messageCode = required.item.itemName
messageCode = required.itemName
messageCode = required.java.lang.String
messageCode = required
BindingResult.rejectValue()
가 내부적으로 MessageCodesResolver
를 사용해 messageCodes를 얻는다rejectValue()
가 new FieldError("item", "itemName", null, false, messageCodes, null, null);
를 실행시킨다필드 오류의 경우, 기본 메세지 생성 규칙은 아래와 같다 ( 구체적인 것을 먼저 만든다 )
errorCode.오브젝트이름.필드명
errorCode.필드명
errorCode.type
errorCode
th:errors
가 실행되는데 th:errors
가 생성된 오류 메세지 코드를 순서대로 돌아가면서 메세지를 찾게 되고, 찾지 못하면 디폴트 메세지를 출력한다검증이 실패한 경우의 처리를 위해 BindingResult
를 사용 ( 3번 )
BindingResult.addError()
를 이용하여 FieldError, ObjectError 객체를 만들어서 BindingResult
에 등록시키는 방식을 사용FieldError
, ObjectError
객체를 생성하는 과정에서 생성자에 메세지를 직접 입력하는 것이 아닌 메세지 파일을 만들고 그 안에서 오류 메세지 내용을 찾도록 변경 ( 5번 )
FieldError, ObjectError 객체를 생성자로 생성하지 않고 메서드를 이용해 간단하게 생성 ( 5-3 번 )
BindingResult.rejectValue()
, BindingResult.reject()
를 이용하는데 기본적으로 필드명과 에러 코드를 파라미터로 받는다rejectValue()
, reject()
동작 살펴보기 ( 6번 )
내부적으로 MessageCodesResolver
를 이용한다
MessageCodesResolver
는 resolveMessageCodes()
메서드를 이용해 에러코드를 바탕으로 메세지 코드들을 생성해 String[]으로 반환 ( 필드 오류의 경우 4가지, 객체 오류의 경우 2가지 )
메세지 코드를 생성할 때 구체적인 것부터 덜 구체적인 것 순으로 생성된다
반환된 메세지 코드들을 이용해 FieldError, ObjectError 객체를 생성한다
그렇게 생성된 FieldError, ObjectError 객체는 codes로 String[] 을 가지고 있는데 th:errors
가 실행될 때 이 String[]을 순서대로 돌아가면서 메세지를 찾게 되고 찾지 못하면 디폴트 메세지를 출력하는 것이다
검증 오류 코드의 두 가지
개발자가 직접 설정 ➜ rejectValue()
를 직접 호출
스프링이 자동으로 추가 ( 타입 정보 불일치 등 )
스프링이 자동으로 추가하는 검증 오류 코드 ( typeMismatch
)
typeMismatch.객체명.필드명
typeMismatch.필드명
typeMismatch.타입
typeMismatch
메세지 설정 파일에 위의 typeMismatch 검증 오류 코드에 해당하는 메세지 코드가 없으면 스프링이 생성한 기본 메세지가 출력
➜ 메세지 파일에 위의 검증 오류 코드와 메세지를 추가하면 스프링이 자동으로 처리하는 에러 메세지도 관리할 수 있다
@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) {
// Controller에 있던 검증 로직
...
}
}
@Component
를 통해 Validator를 스프링 빈으로 등록 ➜ Controller에서 사용할 수 있도록 하기 위해supports()
해당 검증기를 지원하는지 여부를 확인
파라미터로 넘어오는 클래스가 Item 클래스인지, Item 클래스의 자식 클래스인지 판단 ( 언급한 두 경우라면 true가 반환 )
validate()
검증 로직을 처리하는 부분
target은 Controller에서 @ModelAttribute
가 붙은 객체를 전달받아야 한다
errors는 Controller의 BindingResult
객체를 전달 받는다 ( Errors가 BindingResult의 부모이기 때문에 가능 )
객체를 Object로 받았기 때문에 형 변환이 필요 ( Item item = (Item) target;
)
@Controller
@RequiredArgsConstructor
public class ValidationItemControllerV2 {
private final ItemValidator itemValidator;
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
itemValidator.validate(item, bindingResult);
...
}
}
스프링 빈으로 등록된 validator 객체를 주입 받아 사용
주입받은 객체의 validate()
를 호출해서 검증기를 실행시킨다
WebDataBinder
public class ValidationItemControllerV2 {
private final ItemValidator itemValidator;
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
}
Controller가 호출될 때 @InitBinder
가 붙은 init()
메서드가 호출되면서 WebDataBinder
가 만들어진다
WebDataBinder
는 사용자 요청이 올 때마다 새로 생성된다
@InitBinder
는 해당 Controller에만 영향을 준다
WebDataBinder
에 itemValidator를 넣어준다 ➜ 어떤 메서드가 호출되도 항상 검증기를 적용할 수 있다WebDataBinder
에 검증기를 추가하면 해당 Controller에서는 검증기를 자동으로 적용할 수 있다@Validated
이용public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
...
}
@Validated
이 어노테이션이 붙으면 WebDataBinder 에 등록한 검증기를 찾아서 실행한다
@ModelAttribute
가 붙은 대상에 대해 validate()
를 호출하지 않아도 자동으로 찾아진 검증기가 수행된다
supports()
가 사용된다@Validated
는 스프링 전용 검증 어노테이션이고 @Valid
는 자바 표준 검증 어노테이션이다. @Valid
를 사용하려면 의존관계 추가가 필요하다@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
Controller 내부에서 @Validated
를 사용하면 어떤 Controller 인지에 관계 없이 검증기가 실행
supports()
가 먼저 수행글로벌 설정을 하면 다음 게시글의 BeanValidator 가 자동 등록되지 않는다
@InitBinder("targetObject")
public void initTargetObject(WebDataBinder webDataBinder) {
log.info("webDataBinder={}, target={}", webDataBinder, webDataBinder.getTarget());
webDataBinder.addValidators(/*TargetObject 관련 검증기*/);
}
@InitBinder("sameObject")
public void initSameObject(WebDataBinder webDataBinder) {
log.info("webDataBinder={}, target={}", webDataBinder, webDataBinder.getTarget());
webDataBinder.addValidators(/*SameObject 관련 검증기*/);
}
일반적으로 컨트롤러를 만들 때 하나의 컨트롤러는 하나의 모델 객체(Command 객체)를 사용한다
하지만 여러 모델 객체를 사용하고 싶은 경우, 위의 코드처럼 이름을 지정해야한다
@InitBinder
에 이름을 넣어주면 해당 모델 객체에만 영향을 주고, 이름을 넣지 않으면 모든 모델 객체에 영향을 준다