검증 직접 처리
...
Map<String, String> errors = new HashMap<>();
// 필드 오류
if (item.getQuantity() == null || item.getQuantity() >= 10000) {
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);
}
}
...
이렇게 필드 오류와 글로벌 오류가 발생하면 errors 에 담아,
errors 에 값이 있으면 다시 입력 폼이 있는 뷰 템플릿으로 보낸다.
...
<!-- 필드 오류 -->
<input type="text" th:classappend="${errors?.containsKey('quantity')} ? 'field-error' : _"
class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:if="${errors?.containsKey('quantity')}"
th:text="${errors['quantity']}">수량 필드</div>
<!-- 글로벌 오류 -->
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">글로벌 오류 메시지</p>
</div>
...
뷰 템플릿에서 errors 를 찾아서 값을 불러온다.
문제점
- 뷰 템플릿에서의 중복 처리가 많다.
- 타입 오류 처리 불가
- 타입 오류 시 고객이 입력한 값도 별도로 관리되어야 함
BindingResult
객체의 값이 잘못되면 BindingResult 에 담는다.
이때 BindingResult 는 @ModelAttribute 파라미터 다음에 위치해야 한다.
// 필드 오류
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));
}
}
필드 오류의 경우
new FieldError(`objectName`, `field`, `defaultMessage`) 를 생성하고,
글로벌 오류의 경우
new ObjectError(`objectName`, `defaultMessage`) 를 생성하여 `bindingResult` 에 담는다.
그리고 타입 오류가 발생해도 오류 정보를 `bindingResult` 에 담아 컨트롤러를 정상 호출한다.
`objectName` : @ModelAttribute 이름
`field` : 오류가 발생한 필드 이름
`defaultMessage` : 오류 기본 메시지
이 때 `bindingResult` 는 model 에 자동으로 포함된다.
...
<!-- 필드 오류 -->
<input type="text" th:errorclass="field-error"
class="form-control" placeholder="수량을 입력하세요">
<div class="field-error" th:errors="*{itemName}">수량 필드 오류</div>
<!-- 글로벌 오류 -->
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.glabalErrors()}"
th:text="${err}">글로벌 오류</p>
</div>
...
bindingResult 를 사용했기 때문에
`#fields`, `th:errors`, `th:errorclass` 를 사용하여 간편하게 출력할 수 있다.
BindingResult 와 Errors
BindingResult 는 인터페이스이고, Errors 인터페이스를 상속받고 있다.
구현체 BeanPropertyBindingResult 는 둘 다 구현하고 있으므로, 모두 사용할 수 있다.
하지만 Errors 는 단순 오류 저장/조회 기능만 제공하므로,
추가적인 기능들을 제공하는 BindingResult 를 많이 사용한다.
문제점
- 오류 발생시 입력한 값이 유지되지 않는다.
FieldError, ObjectError
FieldError 는 두 가지 생성자를 제공한다.
new FieldError(`objectName`, `field`, `defualtMessage`) (위에서 사용)
new FieldError(`objectName`, `field`, `rejectedValue`, `bindingFailure`, `codes`, `arguments`, `defualtMessage`)
(ObjectError 도 유사하게 두 가지 생성자를 제공한다.)
`objectName` : @ModelAttribute 이름
`field` : 오류가 발생한 필드 이름
`rejectedValue` : 사용자가 입력한 값(거절된 값)
`bindingFailure` : 바인딩 실패인지 검증 실패인지
`codes` : 메시지 코드
`arguments` : 메시지 코드의 매개변수
`defaultMessage` : 오류 기본 메시지
> 참고 <
바인딩 실패 : 타입이 맞지 않는 경우
검증 실패 : 비즈니스 범위에 맞지 않는 경우
동작 흐름
타입 오류로 인한 바인딩이 실패하면,
1. bindingResult 가 FiedError 를 생성하여 `rejectedValue` 에 해당 값을 담아둔다.
2. 컨트롤러가 호출되어 @ModelAttribute 의 객체에 null 이 입력된다.
3. 타임리프의 th:field 는 오류가 발생하면 FieldError 에서 보관한 `rejectedValue` 를 사용하여 출력한다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
// 필드 오류
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));
}
}
메시지/국제화를 통한 MessageSource 를 찾아 메시지를 조회하여 오류 메시지를 체계적으로 사용할 수 있다.
문제점
- FieldError, ObjectError 너무 복잡
rejectValue( ), reject( )
BindingResult 는 검증할 객체 target 바로 다음에 오기 때문에, 검증할 객체의 정보를 이미 알고 있다.
따라서 FieldError, ObjectError 를 직접 생성하지 않고,
BindingResult 의 `rejectValue()`, `reject()` 를 사용하면 깔끔하게 사용할 수 있다.
필드 오류
bindingResult.rejectValue(`field`, `errorCode`, `errorArgs`, `defaultMessage`)
글로벌 오류
bindingResult.reject(`errorCode`, `errorArgs`, `defaultMessage`)
`field` : 오류가 발생한 필드 이름
`errorCode` : 오류 코드 (MessageCodeResolver 를 위한 오류 코드)
`errorArgs` : 메시지의 매개변수
`defaultMessage` : 오류 기본 메시지
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
// 필드 오류
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);
}
}
bindingResult 는 검증 대상 객체를 이미 알고 있기 때문에,
target 에 대한 정보 없이 `reject()` 와 `rejectValue()` 로 간단하게 사용할 수 있다.
이때, MessageCodeResolver 가 field 와 errorCode 만으로 다양한 오류 코드를 만들어 넣어준다.
MessageCodeResolver 란 ?
MessageCodeResolver 가 인터페이스이고, 기본 구현체는 DefaultMessageCodesResolver 이다.
BindingResult 의 rejectValue( ), reject( ) 는 내부에서 MessageCodeResolver 를 사용하여 오류 코드들을 생성한다.
FieldError( ), ObjectError( ) 를 직접 생성하여 파라미터로 넣어줬던 String[ ] 의 메시지 코드들을
MessageCodeResolver 가 이미 알고 있는 검증할 객체와 파라미터를 통해 자동으로 만들어 주는 것이다.
예를 들어 FieldError `rejectValue("itemName", "required")` 라면,
`itemName`, `required` 와 검증할 객체 `item`, itemName 의 `type` 을 사용하여
1. `required.item.itemName`
2. `required.itemName`
3. `required.java.lang.String`
4. `required`
예를 들어 ObjectError `reject("totalPriceMin")` 이라면,
`totalPriceMin` 과 검증할 객체 `item` 을 사용하여
1. `totalPriceMin.item`
2. `totalPriceMin`
우선순위를 만들어 생성한다.
Validator 와 @Validated
검증 로직을 Validator 인터페이스를 구현하여 별도의 클래스에 분리한다.
Validator 인터페이스에는 `supports()` 와 `validate()` 메서드가 있다.
`supports()` : 해당 검증기를 지원하는 여부 확인 (해당 클래스 + 자식 클래스)
`validate(Object target, Errors errors)` : 검증 대상 객체와 BindingResult
(아까 BindingResult 는 Errors 를 상속받았다고 했다.)
컨트롤러에서 스프링 빈으로 주입받아 직접 호출할 수 있다.
자동 호출 - WebDataBinder, @InitBinder, @Vaildated
WebDataBinder 를 사용하여 검증기를 미리 등록하면 해당 컨트롤러에서 자동으로 검증기를 실행할 수 있다.
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
@Validated
그리고 검증할 객체인 @ModelAttribute 애노테이션 앞에 @Vaildated 애노테이션을 추가하면,
WebDataBinder 에 등록한 검증기를 찾아서 실행한다.
이때 검증기의 `supports()` 를 사용하여 검증할 객체에 맞는 검증기를 찾고, `validate()` 가 호출되어 검증한다.
> 참고 <
- @Validated vs @Valid
둘 다 사용 가능하지만, @Valid 는 bulid.gradle 에 의존관계를 추가해야 한다.
@Validated 는 스프링 전용 검증 애노테이션이고, @Valid 는 자바 표준 검증 애노테이션이다.
@Validated 는 내부에 groups 기능을 포함하고 있다. (하지만 잘 사용하지 않음)
> 참고 <
- @MoedlAttribute vs HttpMessageConver 를 사용하는 것들 (@RequestBody, HttpEntity 등)
HttpMessageConvertor 는 전체 객체 단위로 적용되기 때문에, 특정 필드 하나가 바인딩 되지 않으면 데이터를 객체로 변경하지 못한다. 따라서 이후 단계가 실행되지 않아 BindingResult 에 담기지도 않고, Validator 도 적용할 수 없다.
하지만 @ModelAttribute 는 필드 단위로 정교하게 바인딩이 적용되어, 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩이 되며 BindingResult 에도 담기고, Validator 도 적용할 수 있다.
출처 | 스프링 MVC 2(김영한) - 인프런
'💠프로그래밍 언어 > Java' 카테고리의 다른 글
[Spring] 쿠키/세션으로 로그인 처리하기 (세션 저장소 만들기, HttpSession) (0) | 2025.04.03 |
---|---|
[Spring] Bean Validation 사용하여 간편하게 검증 로직 추가하기 ! (0) | 2025.04.03 |
[Spring] 메시지 / 국제화 하기 (0) | 2025.03.31 |
[Thymeleaf] 타임리프 사용 방법 완전 정복! (0) | 2025.03.20 |
[Spring] SpringMVC 의 요청 매핑과 요청/응답 메시지 기능 ! (0) | 2025.03.20 |