스프링 부트 3.2.1 버전을 기준으로 작성됨
대부분이 실습코드이므로 학습위주로 정리함
컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증 하는 것
참고: 클라이언트 검증, 서버 검증
클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함
// 생으로 if문 걸어서 값 null 체크 및 값 체크 등
// Map에 error를 직접 담아서 모델에 보낸다.
@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 (...) {
if (...) {
errors.put("globalError", "...");
}
}
//검증에 실패하면 다시 입력 폼으로
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
//성공 로직
th:if="${errors?.containsKey('globalError')}"
th:text="${errors['globalError']}"
이런식으로 에러 코드를 넣어 있다면 에러를 출력
참고 Safe Navigation Operator
errors?.
은 errors가null
일 때NullPointerException
대신null
을 반환하는 문법
th:if
에서null
은 실패로 처리 -> 오류 메시지 출력 x
필드 오류 처리
<input type="text" th:classappend="${errors?.containsKey('itemName')} ? 'fielderror' : _" class="form-control">
값이 없으면 _(No-Operation)을 사용해서 동작 무시
정리
- 검증 오류 발생시 입력 폼으로
- 검증 오류들을 고객에게 안내 및 다시 입력
- 검증 오류가 발생해도 입력 데이터 유지
문제점
- 뷰 템플릿 중복 처리가 많음
- 타입 오류 처리가 안됨 -> 400 에러
- 타입 오류로 잘못 입력한 값도 입력 데이터를 유지해야함
- 고객이 입력한 값을 별도로 관리해야함
스프링이 제공하는 검증 오류 처리
BindingResult
BindingResult
에 담아서 컨트롤러 정상 호출@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.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";
}
//성공 로직
}
주의
BindingResult bindingResult
파라미터의 위치는 @ModelAttribute Item item
다음에 와야 한다.
/** Field Error 생성자 요약
* objectName : @ModelAttribute 이름
* field : 오류가 발생한 필드 이름
* defaultMessage : 오류 기본 메시지
*/
public FieldError(String objectName, String field, String defaultMessage) {}
/** 글로벌 오류 ObjectError
* objectName : @ModelAttribute 이름
* defaultMessage : 오류 기본 메시지
*/
public ObjectError(String objectName, String defaultMessage) {}
사용자 입력 데이터 유지
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.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";
}
//성공 로직
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)
* objectName : 오류가 발생한 객체 이름
* field : 오류 필드
* rejectedValue : 사용자가 입력한 값(거절된 값)
* bindingFailure : 타입 오류 같은 바인딩 실패인지 검증 실패인지 구분 값 (false가 검증 실패)
* codes : 메시지 코드 String[]
* arguments : 메시지에서 사용하는 인자 Object[]
* defaultMessage : 기본 오류 메시지
th:field="*{price}"
FieldError
에서 보관한 값 사용해서 출력FieldError
를 생성 및 사용자 값 저장한다.BindingResult
에 담아서 컨트롤러 호출타임리프는 스프링의 BindingResult 를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공
// 글로벌 오류 처리
<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>
#fields
: #fields 로 BindingResult 가 제공하는 검증 오류에 접근 가능
th:errors
: 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전
th:errorclass
: th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가
spring.messages.basename=messages,errors 추가
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
new FieldError("item", "price", item.getPrice(), false, new String[]
{"range.item.price"}, new Object[]{1000, 1000000}
FieldError , ObjectError 를 직접 생성하지 않고, 깔끔하게 검증 오류 다루기 가능
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
* field : 오류 필드명
* errorCode : 오류 코드 (메시지에 등록된 코드 x )
* errorArgs : 오류 메시지에서 {0} 등을 치환하기 위한 값
* defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
MessageCodesResolver
의 기능
객체명과 필드명을 조합한 세밀한 메시지 코드가 있으면 높은 우선순위를 갖는다.
검증 오류 코드로 메시지 코드들을 생성한다.
MessageCodesResolver 인터페이스이고 DefaultMessageCodesResolver 는 기본 구현체이다.
주로 다음과 함께 사용 ObjectError , FieldError
객체 오류의 경우 다음 순서로 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"
타임리프 화면을 렌더링 할 때 th:errors 가 실행
오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 조회
없으면 디폴트 메시지를 출력
// 사용 전
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은 필수입니다.");
}
// 사용 후
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
스프링은 타입 오류가 발생하면 typeMismatch
라는 오류 코드를 사용
// 4가지 메시지 코드 생성
typeMismatch.item.price
typeMismatch.price
typeMismatch.java.lang.Integer
typeMismatch
타입 에러를 일으키면
Failed to convert property value of type java.lang.String to required type
java.lang.Integer for property price; nested exception is
java.lang.NumberFormatException: For input string: "A"
이런 문구가 뜬다. 이걸 변경해보자
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
이렇게 하면 원하는 메시지를 출력 가능
검증 로직을 별도로 분리
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
BindingResult
하나의 검증 클래스를 만들고 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;
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.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);
WebDataBinder
사용
스프링의 파라미터 바인딩의 역할 및 검증 기능도 내부에 포함
@InitBinder // 해당 컨트롤러에서 검증기 자동 적용
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 깔끔...
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
@Validated @ModelAttribute Item item
✅ 참고
@Validated 는 스프링 전용
@Valid 는 자바 표준 검증 애노테이션
둘다 사용 가능하다.
🔖 학습내용 출처