[Spring] 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 섹션4 검증1 - Validation
[Spring] 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 섹션4 검증1 - Validation
◼️ 검증 요구사항
요구사항: 검증 로직 추가
- 타입 검증
- 가격, 수량에 문자가 들어가면 검증 오류 처리
- 필드 검증
- 상품명: 필수, 공백X
- 가격: 1000원 이상, 1백만원 이하
- 수량: 최대 9999
- 특정 필드의 범위를 넘어서는 검증
- 가격 * 수량의 합은 10,000원 이상
컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
참고: 클라이언트 검증, 서버 검증
클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수 API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함
◼️ 검증 직접 처리
서버검증 로직이 실패하면 고객에게 다시 상품 등록 폼을 보여주고, 어떤 값을 잘못 입력했는지 알려줘야한다.
검증 오류 발생시 errors에 담고, 뷰에서 해당 데이터를 사용해 오류메시지를 출력해보자.
- 만약 검증 오류가 발생하면 오류들과 입력 폼을 다시 보여준다.
- 검증 오류가 발생해도 고객이 입력한 데이터가 유지된다.
@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("errors = {}",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}";
}
HTML은 아래와같은 형식으로 출력한다.
?는 errors가 null이면 NullPointerException대신 null을 반환한다.
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>
🟢 남은 문제점
- 뷰 템플릿에서 중복 처리가 많다.
- 하지만 위 문제점은 타입 오류 처리가 안된다는 것이다. 타입오류는 MVC에서 컨트롤러 진입전 예외가 발생하기 때문에, 컨트롤러가 호출되지도않고 400예외가 발생한다.
- 결국 고객이 입력한 값도 어딘가에 별도로 관리가 되어야 한다.
◼️ BindingResult1
ValidationItemControllerV2 - addItemV1
@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
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() >= 10000) {
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("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}";
}
BindingResult bindingResult 파라미터의 위치는 @ModelAttribute Item item 다음에 와야 한다.
🟢 FieldError 생성자
// @ModelAttribute명,오류발생 필드명,오류기본메시지
public FieldError(String objectName, String field, String defaultMessage) {}
사용 예시 (필드 오류 - FieldError)
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
🟢 ObjectError 생성자
// @ModelAttribute명, 오류 기본 메시지
public ObjectError(String objectName, String defaultMessage) {}
사용 예시 (글로벌 오류 - ObjectError)
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야합니다. 현재 값 = " + resultPrice));
이제 validation/v2/addForm.html 을 아래와 같은 형식으로 수정한다.
글로벌 오류 처리 예시
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
</div>
필드 오류 처리 예시
<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">
상품명 오류
</div>
타임리프는 스프링의 BindingResult 를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공한다.
- #fields : #fields 로 BindingResult 가 제공하는 검증 오류에 접근할 수 있다.
- th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전이다.
- th:errorclass : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.
검증과 오류 메시지 공식 메뉴얼
https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#validation-and-error-messages
◼️ BindingResult2
BindingResult 가 있으면 @ModelAttribute 에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다.
- BindingResult 가 없으면 400 오류가 발생하면서 컨트롤러가 호출되지 않고, 오류 페이지로 이동한다.
- BindingResult 가 있으면 오류 정보( FieldError )를 BindingResult 에 담아서 컨트롤러를 정상 호출한다.
- BindingResult 는 검증할 대상 바로 다음에 와야한다.
- BindingResult 는 Model에 자동으로 포함된다.
- BindingResult 는 인터페이스이고, Errors 인터페이스를 상속받고 있다.
🟢 BindingResult에 검증 오류를 적용하는 3가지 방법
- @ModelAttribute 의 객체에 타입 오류 등으로 바인딩이 실패하는 경우 FieldError 생성해서BindingResult 에 넣어준다.
- 개발자가 직접 넣어준다.
- Validator 사용 (뒤에서 다룸)
◼️ FieldError, ObjectError
사용자 입력 오류 메시지와 고객이 입력한 내용이 모두 화면에 남도록 하자.
@PostMapping("/add")
public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
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() >= 10000) {
bindingResult.addError(new FieldError("item", "quantity",item.getPrice(),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("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}";
}
🟢 FieldError 생성자
FieldError는 오류 발생시 사용자 입력 값을 저장하는 기능을 제공한다.
- objectName: 오류가 발생한 객체 이름
- field: 오류 필드
- rejectedValue: 사용자 입력값(거절된 값. 오류 발생시 사용자 입력 값을 저장하는 필드다.)
- bindingFailure: 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값 (타입 오류 같은 바인딩이 실패했는지 여부를 적는다. 바인딩실패가 아니면 false 를 사용한다)
- codes: 메시지 코드
- arguments: 메시지에서 사용하는 인자
- defaultMessage: 기본 오류 메시지
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)
사용자의 입력 데이터가 컨트롤러의 @ModelAttribute 에 바인딩되는 시점에 오류가 발생하면 모델 객체에 사용자입력 값을 유지하기 어렵다. (ex_타입에러) 그래서 오류가 발생한 경우 사용자 입력 값을 따로 별도 보관했다가 검증 오류시 화면에 다시 출력한다.
🟢 타임리프의 사용자 입력 값 유지
th:field="*{price}"
타임리프의 th:field 는 정상 상황에는 모델 객체의 값을 사용하지만, 오류가 발생하면 FieldError 에서 보관한 값을 사용해서 값을 출력한다.
🟢 스프링의 바인딩 오류 처리
타입 오류로 바인딩에 실패하면 스프링은 FieldError 를 생성하면서 사용자가 입력한 값을 넣어둔다.
그리고 해당 오류를 BindingResult 에 담아서 컨트롤러를 호출한다.
따라서 타입 오류 같은 바인딩 실패시에도 사용자의 오류 메시지를 정상 출력할 수 있다.
◼️ 오류 코드와 메시지 처리1
🟢 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)
errors 메시지 파일 생성
오류 메시지를 구분하기 쉽게 errors.properties 라는 별도의 파일로 관리해보자.
( errors_en.properties 파일을 생성하면 오류 메시지도 국제화 처리를 할 수 있다.)
먼저 스프링 부트가 해당 메시지 파일을 인식할 수 있게 다음 설정을 추가한다.
application.properties
spring.messages.basename=messages,errors
이렇게하면 messages.properties , errors.properties 두 파일을 모두 인식한다. (messages.properties 를 기본으로 인식한다.)
errors.properties
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
이제 errors 에 등록한 메시지를 사용하도록 코드를 변경해보자.
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
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() >= 10000) {
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));
}
}
...
//range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
new FieldError("item", "price", item.getPrice(), false, new String[] {"range.item.price"},
new Object[]{1000, 1000000});
- codes : required.item.itemName 를 사용해서 메시지 코드를 지정한다. 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
- arguments : Object[]{1000, 1000000} 를 사용해서 코드의 {0} , {1} 로 치환할 값을 전달한다.
◼️ 오류 코드와 메시지 처리2
🟢 목표
FieldError , ObjectError 는 다루기 너무 번거롭다.
오류 코드도 좀 더 자동화 해보자. 예) item.itemName 처럼
컨트롤러에서 BindingResult 는 검증해야 할 객체인 target 바로 다음에 온다. 따라서 BindingResult 는 이 미 본인이 검증해야 할 객체인 target 을 알고 있다.
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
objectName=item //@ModelAttribute name
target=Item(id=null, itemName=상품, price=100, quantity=1234)
rejectValue() , reject()
BindingResult 가 제공하는 rejectValue() , reject() 를 사용하면 FieldError , ObjectError 를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.
/*오류필드명, 오류코드(메시지등록코드X.messageResolver를 위한 오류코드),
치환값,오류메시지 찾을 수 없을때 사용하는 기본메시지*/
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
사용예시
//range.item.price=가격은 {0} ~ {1} 까지 허용합니다. (errors.properties)
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null)
@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}",bindingResult.getTarget());
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() >= 10000) {
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);
}
}
◼️ 오류 코드와 메시지 처리3
세밀한 메시지 코드가 있으면 해당 메시지를 높은 우선순위로 사용한다.
errors.properties
#Level1
required.item.itemName: 상품 이름은 필수 입니다.
#Level2
required: 필수 값 입니다.
◼️ 오류 코드와 메시지 처리4
🟢 MessageCodesResolver
- 검증 오류 코드로 메시지 코드들을 생성한다.
- MessageCodesResolver는 인터페이스이고 DefaultMessageCodesResolver 는 기본 구현체이다.
- 주로 다음과 함께 사용 ObjectError , FieldError
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
void messageCodesResolverObject() {
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
for (String messageCode : messageCodes) {
System.out.println("messageCode ="+messageCode);
}
Assertions.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);
}
// new FieldError("item","itemName",null,false,messageCodes,null,null);
Assertions.assertThat(messageCodes).containsExactly(
"required.item.itemName",
"required.itemName",
"required.java.lang.String",
"required"
);
}
}
🟢 DefaultMessageCodesResolver의 기본 메시지 생성 규칙
객체 오류: 객체 오류의 경우 다음 순서로 2가지 생성
1. code + "." + object name
2. code
예) 오류 코드: required, object name: item
1: required.item
2: required
필드 오류: 필드 오류의 경우 다음 순서로 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 , ObjectError 의 생성자를 보면, 여러 오류 코드를 가질 수 있다.
- MessageCodesResolver 를 통해서 생성된 순서대로 오류 코드를 보관한다.
- 이 부분을 BindingResult 의 로그를 통해서 확인해보자.
codes [range.item.price, range.price, range.java.lang.Integer, range]
- 자동생성 오류 코드 예시
FieldError rejectValue("itemName", "required") | ObjectError reject("totalPriceMin") |
required.item.itemName required.itemName required.java.lang.String required |
totalPriceMin.item totalPriceMin |
오류 메시지 출력
타임리프 화면을 렌더링 할 때 th:errors 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지 코드를 순서대 로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다.
◼️ 오류 코드와 메시지 처리5
🟢 오류코드 관리 전략
- 구체적인 것에서 덜 구체적인 것으로! required.item.itemName, required
- 크게중요X 메시지는 범용성있는 required같은 메시지로 끝내고, 정말 중요한 메시지는 꼭 필요할때 구체적으로 적어 사용하자.
전략을 적용한 errors.properties 예시
#required.item.itemName=상품 이름은 필수입니다.
#range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
#max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==ObjectError==
#Level1
totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#Level2 - 생략
totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==FieldError==
#Level1
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#Level2 - 생략
#Level3
required.java.lang.String = 필수 문자입니다.
required.java.lang.Integer = 필수 숫자입니다.
min.java.lang.String = {0} 이상의 문자를 입력해주세요.
min.java.lang.Integer = {0} 이상의 숫자를 입력해주세요.
range.java.lang.String = {0} ~ {1} 까지의 문자를 입력해주세요.
range.java.lang.Integer = {0} ~ {1} 까지의 숫자를 입력해주세요.
max.java.lang.String = {0} 까지의 문자를 허용합니다.
max.java.lang.Integer = {0} 까지의 숫자를 허용합니다.
#Level4
required = 필수 값 입니다.
min= {0} 이상이어야 합니다.
range= {0} ~ {1} 범위를 허용합니다.
max= {0} 까지 허용합니다.
🟢 ValidationUtils
Empty,공백 같은 단순 기능 제공
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은 필수입니다.");
}
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
🟢 동작방식 정리
1. rejectValue() 호출
2. MessageCodesResolver 를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3. new FieldError() 를 생성하면서 메시지 코드들을 보관
4. th:erros 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출
◼️ 오류 코드와 메시지 처리6
🟢 검증 오류 코드 종류
- 개발자가 직접 설정한 오류 코드 rejectValue() 를 직접 호출
- 스프링이 직접 검증 오류에 추가 (주로 타입 정보가 맞지 않음)
스프링은 타입오류가 발생하면 typeMismatch라는 오류코드를 사용한다.
MessageCodesResolver 는 이를 통해 4가지 메시지 코드를 생성한다.
- typeMismatch.item.price
- typeMismatch.price
- typeMismatch.java.lang.Integer
- typeMismatch
숫자필드에 문자를 넣으면 아래 오류가 출력됨을 확인 가능하다.
이제 error.properties에 아래를 추가하고 실행해본다.
errors.properties
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다
소스코드를 건들지않고 원하는 메시지를 단계별로 설정할 수 있다.
메시지 코드 생성 전략은 Bean Validation을 학습하면 진가를 확인할 수 있다.
◼️ Validator 분리1
🟢 목표
- 복잡한 검증로직을 별도 클래스로 분리하자.
스프링은 체계적 검증을 위해 검증 인터페이스를 제공한다.
public interface Validator {
boolean supports(Class<?> clazz); // 해당 검증기 지원 여부 확인
void validate(Object target, Errors errors); // 검증 대상 객체와 BindingResult
}
ItemValidator
@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;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "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() > 10000) {
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);
}
}
}
}
이렇게 하면 컨트롤러 검증부분을 깔끔히 코드 작성할 수 있다!
private final ItemValidator itemValidator;
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
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}";
}...
◼️ Validator 분리2
🟢 WebDataBinder
- 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.
- WebDataBinder에 검증기를 추가하면 컨트롤러에서 검증기를 자동 적용할수 있다.
@InitBinder //해당 컨트롤러에만 영향을 줌.글로벌 X
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
@Validated 어노테이션을 넣는다.이는 검증기를 실행하는 애노테이션이다.
WebDataBinder에 등록한 검증기를 찾아 실행한다.
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult
bindingResult, RedirectAttributes redirectAttributes) {
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}";
}
🟢 글로벌 설정 - 모든 컨트롤러에 다 적용
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
글로벌 설정을 하면 다음에 설명할 BeanValidator가 자동 등록되지 않는다. 글로벌 설정 부분은 주석처리해둔다. 참고로 글로벌 설정을 직접 사용하는 경우는 드물다.
검증시 @Validated @Valid 둘다 사용가능
javax.validation.@Valid 를 사용하려면 build.gradle 의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Validated 는 스프링 전용 검증 애노테이션이고, @Valid 는 자바 표준 검증 애노테이션이다.