-
SpringMVC 2 - 검증Web/Spring 2023. 10. 24. 22:17
- 고객이 입력한 데이터를 유지한 채 어떤 오류가 발생했는지 친절하게 알려주자
- 컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것
1. 검증 V1 (직접검증)
- 사용자의 데이터가 정상이면, PRG
- 정상이 아니면, 모델에 사용자 입력 값과 오류 결과를 담아서 다시 상품등록폼으로 가자
Map<String, String> errors = new HashMap<>(); // 오류 담을 것
1.1 특정 필드 검증로직
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 까지 허용합니다."); }
1.2 복합 필드 검증
//특정 필드가 아닌 복합 룰 검증 if (item.getPrice() != null && item.getQuantity() != null) { int resultPrice = item.getPrice() * item.getQuantity(); if (resultPrice < 10000) { errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice); } }
* StringUtils는 Spring꺼 쓰자
//검증에 실패하면 다시 입력 폼으로 if (!errors.isEmpty()) { model.addAttribute("errors", errors); return "validation/v1/addForm"; }
- 이때 validation/v1/addForm으로 forward! -> request,response가지고감 + model가지고감 (내부호출)
- @ModelAttribute 사용해서 Intem item -> item으로 저장되어 있음
* /add + GetMapping에도 빈 item객체가 넘어가도록 설정해둠!! -> 오류처리 등의 재사용성을 높이기 위함
1.3 addform with 타임리프
- 글로벌 오류
<div th:if="${errors?.containsKey('globalError')}"> <p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p> </div>
- if errors에 globalError이 있을 때 -> div태그와 내용 출력
- errors?는 errors이 null이면 실행하지 않도록 하는 타임리프 문법 (NPE 대신 null반환)
* key값을 확인하는 것과 값을 꺼내는 것 다름주의
//필드오류처리 th:class="${errors?.containsKey('itemName')} ? 'form-control field-error' : 'form-control'" <div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="${errors['itemName']}"> 상품명 오류 </div>
<input type="text" th:classappend="${errors?.containsKey('itemName')} ? 'fielderror' : _" class="form-control">
- 주요 로직은 그냥 errors?.containsKey 있다면 조건으로
th:classappend나 th:text, th:if절 사용했다는 것
-> 직접 검증처리시 문제점은 뷰 템플릿에 중복처리가 많다.
-> 타입 오류처리가 안됨!! (애초에 Integer에 String이 들어오는 등) 컨트롤러로 들어오기 전 발생한 오류임
-> 이러한 문제를 해결한 Spring의 오류처리를 보자
2. 검증V2 (BindingResult)
- 스프링이 제공하는 검증 오류 처리의 핵심 BindingResult를 알아보자
- BindingResult : 에러코드를 담아서 뷰로 같이 넘어감 (따로 Model에 담아 줄 필요 없음)
- BindingResult는 검증이 필요한 파라미터 바로 다음에 와야함
//필드 오류 if (!StringUtils.hasText(item.getItemName())) { bindingResult.addError(new FieldError("item", "itemName", "상품 이름은필수입니다.")); }
> 매개변수로, 객체명(@ModelAttribute 이름), 필드명, 기본오류메시지를 받는다.
> 필드오류는 FieldError
//글로벌오류 bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
> 매개변수로 객체명(@ModelAttribute), 기본 오류 메시지를 받음
> globalError은 ObjectError
2.1 타임리프 오류 검증 통합 기능
타임리프에서 BindingResult를 활용할 수 있는 편의 기능 제공한다.
#fields로 BindingResult가 제공하는 검증 오류에 접근 가능 (view로 넘어온 BindingResult 활용)
th:errors: 오류가 있는 경우 해당 태그 출력 -> if 상위버전
th:errorclass: 필드에 오류가 있다면, class정보 추가 -> if상위버전
*th:object를 지정하면 th:field = *{필드} 혹은 *{필드} 사용할 수 있음!! object로 지정한 객체의 특정 속성 꺼내줌
*은 선택지정자 (특정)
*타임리프가 위와 같은 문법을 사용하므로, Field나 Object Error에서 객체 혹은 필드명 잘 지정해줘야 출력됨
2.2 글로벌 오류처리
<div th:if="${#fields.hasGlobalErrors()}"> <p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="$ {err}">전체 오류 메시지</p> </div>
2.3 필드 오류 처리
<input type="text" id="itemName" th:field="*{itemName}" th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요"> <div class="field-error" th:errors="*{itemName}"> 상품명 오류 </div>
3. BindingResult 더 알아보기
- BindingResult
- 스프링이 제공하는 검증 오류 보관 객체 -> BindingResult가 있으면, ModelAttribute에 데이터 바인딩 시 오류가 발생해도
컨트롤러가 호출됨!
- 오류정보(FieldError)을 알아서 만들어서 BindingResult에 담아서 컨트롤러 정상 호출
* BindingResult에 검증오류 작성 3가지 방법
> 스프링이 타입오류 넣어줌
> 직접 Field,Object error을 만들어서 넣기
> Validator를 사용
*BindingResult와 Errors
-> BindingResult는 인터페이스, Errors인터페이스 상속받음 : 구현객체 BeanPropertyBindigResult임
-> 인터페이스로 둘 중 아무거나 사용해도됨 단, BindingResult가 더 다양한 기능을 제공함
4. 검증V3
-> 현재 V2까지 문제는 오류메시지는 출력되는데, 사용자가 입력한 오류 값은 출력이 안됨 해결하자
-> bindingResult는 사용자 오류의 입력값을 저장할 수 있는 매개변수 제공함 (공간?)
public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage)
- ObjectName: @ModelAttribute 이름
- field : 오류필드
- rejectedValue : 사용자 입력값
- bindingFailure: 타입 오류인지, 검증 오류인지 구분 값
- codes: 메시지 코드
- argumetns : 메시지에 인자
- defaultMessage: 기본 오류 메시지
*주의 이때 bindingError를 false라고 하면, rejectedValue와 객체의 필드 사이의 타입이 맞아야한다.
> 타입오류 아니라고 했는데.. 거절값이 타입이 안맞는건 말이 안된다.
> 필드명 잘못적으면 저장은 되지만 아마 타임리프에서 출력은 안될 것이다.
* 만약 타입 오류라면, 스프링이 알아서
new FieldError(item,price,"qqq",true..)식으로 넣어준다!
* 향후 간편 오류처리들이 위의 메서드 호출하므로 잘 봐두자
4.1 타임리프 field="*{필드}"
아주 똑똑하게 작용함 정상 상황에서는 모델 객체의 값을 사용하지만, 오류가 발생하면, FieldError에서 보관한 값을 사용해서 출력한다.
5. 오류 코드와 메시지 처리 (1) [그냥 그렇구나]
- 생성자에 codes와 메시지 arguments를 넘길 수 있다! 메시지를 관리해보자
5.1 errors 메시지 파일 생성
- errors.properties파일을 생성해서 오류 메시지를 관리하자
- 메시지 파일을 인식할 수 있도록 아래와 같이 설정
- MessageSource 설정
spring.messages.basename=messages,errors
required.item.itemName=상품 이름은 필수입니다. range.item.price=가격은 {0} ~ {1} 까지 허용합니다. max.item.quantity=수량은 최대 {0} 까지 허용합니다. totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
5.2 매개변수 전달
new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}
- 메시지 코드를 매개변수로 전달해서 해당 코드의 메시지를 출력하도록 할 수 있다. (String[])
- 메시지 매개변수는 object[]로 전달할 수 있다.
6. 오류 코드와 메시지 처리 (2)
- 이제 FieldError,ObjectError보다 쉽게 오류코드를 처리하는 BindingResult에 검증 메서드를 사용하자
- 어차피 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)
6.1 rejectValue(), reject()
- FieldError과 ObjectError를 생성하지 않고, 쉽게 다룰 수 있게 해준다.
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); //실제 사용 bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null) void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
filed : 오류 필드명
errorCode: 오류코드 -> messageResolver를 위한 오류 코드
errorArgs: 오류 메시지 치환 값
defaultMessage:기본 메시지
> rejectValue는 필드명,오류 메시지 코드,메시지매개변수,기본 메시지 받으면
- 내부적으로 new Field만들어주는데, 거절값, 객체명, 오류메시지코드를 자동으로 생성해준다.
7. 오류 코드와 메시지 처리(3) [매우중요]
- 오류 코드를 만들 때 범용성 있게 만들기 위해서는 추상적인 것부터 만들어두고, 조금씩 구체적으로 내려가는 것이 최고다.
- 오류 메시지 코드의 level을 나누어 사용하는 것이다!
- 스프링의 MessageCodeResolver가 이러한 범용성 있는 오류 메시지 만드는 것을 도와준다.
7.1 MessageCodeResolver
- 검증 오류 코드로 메시지 코드를 생성한다.
- 구현체로 DefaultMessageCodesResolver를 가지는 인터페이스
- 주로 ObjectError과 FieldError이랑 함께 사용된다.
7.2 기본 메시지 생성 규칙
객체 오류의 경우 다음 순서로 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를 사용하여, 메시지 코드를 생산해서 주입함
- 보통 가장 구체적인 것부터 저장됨!
*기본 메시지 지정 x시 메시지 코드들로 메시지 못찾으면 Exception발생
7.3 오류 메시지 출력
타임리프 화면을 렌더링시 th:errors가 실행됨 이때 오류가 있다면, 생성된 오류 메시지 코드를 순서대로 돌아가면서
메시지를 찾음! 없으면 디폴트 메시지 출력
7.4 오류 메시지 관리 전략
- 핵심은 구체적인 것에서 덜 구체적인 것으로 (MessageCodesResolver)
- 따라서 공통전략 (가장 낮은 레벨)은 한번에 관리할 수 있다.
- 대부분 공통전략을 따르고 필요한 경우에만 구체적으로 적어서 메시지를 출력하자
#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라는게 있는데 걍 참고
7.5 스프링이 만든 오류 메시지
- 타입 오류의 경우 스프링이 직접 오류 메시지 코드를 정의해 두었다.
typeMismatch.item.price typeMismatch.price typeMismatch.java.lang.Integer typeMismatch
8. Validator (1)
- 컨트롤러에서 검증하는 로직이 너무 크다. 별도의 역할로 분리하여 검증 로직을 재사용하자
- 스프링은 검증 로직을 체계적으로 제공하기 위해 인터페이스 제공함
public interface Validator { boolean supports(Class<?> clazz); void validate(Object target, Errors errors); }
- 해당 검증기가 지원하는 객체인지 확인
- 검증하기 (타겟 객체, bindingResult 넣기)
- 어댑터 패턴과 매우 유사 (내부에 어답티만 없는..)
@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); } } } }
-> 컨트롤러 호출 코드
@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}"; }
9 Validator 분리 (2)
Validator 인터페이스를 사용하여 검증기를 만들면, 스프링의 추가적인 도움을 받을 수 있다.
9.1 WebDataBinder
- 스프링의 파라미터 바인딩 역할 + 검증 기능도 내부에 포함
@InitBinder public void init(WebDataBinder dataBinder) { log.info("init binder {}", dataBinder); dataBinder.addValidators(itemValidator); }
- 위와 같이 검증기를 추가하면, 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.
@InitBinder는 해당 컨트롤러에만 영향을 준다 -> 글로벌 설정은 별도
9.2 Validated 적용
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}"; }
@Validated는 WebDatabinder에 등록된 검증기로 가서 검증기를 실행하라는 어노테이션이다.
이때 WebDatabinder에 많은 검증기가 등록되어 있다면, 지원가능한 검증기 찾아야해서
supports()가 있던 것이었다리
- 여기서는 supports가 true니까 ItemValidator의 validate에 BindingResult와 item객체가 넘어가서 실행됨
*WebDatabinder 글로벌 설정은 PDF참고
'Web > Spring' 카테고리의 다른 글
SpringMVC 2 - 서블릿 필터와 인터셉터 (0) 2023.11.15 Spring MVC 2 - 검증 (2) BeanValidation (0) 2023.10.25 Spring MVC 2 - 국제화/메시지 (0) 2023.10.24 스프링 MVC - 웹 페이지 만들기 (0) 2023.10.15 Spring MVC (6) - 스프링 MVC 기본기능 (2) : Http Body (0) 2023.10.14