검증 요구사항#
웹 서비스는 폼 입력시 오류가 발생하면, 데이터를 유지한 상태로 오류를 사용자에게 알려주어야 한다.
컨트롤러의 중요한 역활 중 하나는, HTTP 요청이 정상인지 검증하는 것이다
- 클라이언트 검증(js)은 보안에 취약
- 서버만으로 검증하면, 고객 사용성 부족
- 적절히 섞어서 사용하되, 최종 서버 검증은 필수
이전 프로젝트를 일부 수정한 validation-start프로젝트에서 진행
검증 처리#
고객이 폼을 잘못 입력한 경우
검증시 오류가 발생하면 errors 해시맵에 담아놓고, 이와함께 폼으로 다시가는식
@ModelAttribute는 사용자가 입력한 데이터를 계속 가지고 있음
** 폼에서 사용자에게 오류 알리기 **
<div th:if="${errors?.containsKey('globalError')}">
<p class="field-error" th:text="${errors['globalError']}">전체 오류 메시지</p>
</div>errors?: 에러가 null이면, NPE대신 null 반환th:if에서 null을 실패로 처리하고 무시 (SpringEL문법)
BindingResult#
스프링이 제공하는 검증 오류를 보관하는 객체
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {주의
BindingResult bindingResult는 무조건 ModelAttribute Item item 뒤어야함
FieldError(String objectName, String field, String defaultmessage)- 특정 필드에 오류가 있을 때
@ModelAttribute이름, 필드이름, 오류 기본메시지
ObjectError(String objectName, String defaultMessage)- 특정 필드를 넘어서는 오류 (글로벌에러)
@ModelAttribute이름, 오류 기본메시지
** 타임리프를 통한 처리** ObjectError(글로벌에러)
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">전체 오류 메시지</p>
</div>FieldError
th:field로 설정한 필드와, bindingResult의 필드가 일치할 때 오류
<div>
<label for="itemName" th:text="#{label.item.itemName}">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
<div class="field-error" th:errors="*{itemName}">
상품명 오류
</div>
</div>
<div>기능정리
#fields:BindingResult가 제공하는 검증 오류에 접근 가능th:errors: 해당 필드에 오류가 있는 경우 태그 출력th:if편의버전th:errorclass: 해당 필드에 오류가 있으면 클래스정보 추가
BindingResult 2#
@ModelAttribute에 바인딩 시 오류가 발생하면?
BindingResult가 있으면 -> 400오류가 발생하면서 오류페이지로 이동BindingResult가 없으면 ->오류정보(FieldError)를BindingResult에 담아서 컨트롤러를 호출
BindingResult에 검증 오류를 적용하는 3가지방법
- 스프링이
FieldError를BindingResult에 넣어줌 - 개발자가 직접 넣어줌
Validator사용
BindingResult는 무조건 검증할 대상 다음에 와야하고, 모델에 자동으로 포함된다
FieldError,ObjectError#
** FieldError 생성자(2개있음)**
ObjectName: 객체 이름field: 오류 필드rejectedValue: 사용자가 입력한 값(거절된)bindingFailure: 바인딩 실패인지, 검증 실패인지 구분codes: 메시지 코드arguments: 메시지에서 사용하는 인자defaultMessage: 기본 오류 메시지
new FieldError("item","itemName",item.getItemName(), false,null,null,"상품 이름은 필수입니다."타입 오류로 바인딩에 실패하면
스프링은 FieldError를 생성하면서 입력값을 넣고,
bindingResult에 사용자 입력 데이터를 담아 컨트롤러 호출
타임리프에서 입력 값 유지
th:field="*{price}"
th:field는 정상일땐 모델 객체의 값을 사용하지만, 오류가 발생하면 FieldError에서 보관한 값을 사용해 출력
오류코드와 메시지 처리 1#
** errors 메시지파일 **
여기서 했던 메시지처럼, errors.properties라는 별도의 파일 생성 후
spring.messages.basename=messages,errors 추가하면
resources 밑에 있는 errors.properties를 참고하게됨
ex)
if(!StringUtils.hasText(item.getItemName())){
bindingResult.addError(new FieldError("item","itemName",item.getItemName(), false,
new String[]{"required.item.itemName"},null,"상품 이름은 필수입니다.")); }codes에 String 배열로 넣어주면 된다.
배열로 받는 이유는, 첫 없으면 다음 원소를 넣어줘서임
argument가 있으면, Object배열로 넣어주면 된다.
오류코드와 메시지 처리 2#
bindingResult는 검증할 객체 바로 다음에 오기 때문에, 검증할 객체를 알고있다.
rejectValue(),reject()#
BindingResult가 제공하는 rejectValue(),reject()로
FieldError,ObjectError를 직접 생성하지 않고 오류를 다룰 수 있다.
바꾼 방식
bindingResult.rejectValue("itemName","required",
new Object[]{1000,1000000},null);
////
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
//오류필드명,messageResolver를 위한 오류 코드, Args,기본메시지오류 코드와 메시지 처리 3#
오류 메시지를 required=필수값 입니다 처럼 단순하게 만들면 범용성이 좋지만, 세밀한 메시지를 작성하기 어렵다.
반대로
required.item.itemName=상품 이름은 필수입니다. 처럼 너무 자세하게 만들면 범용성이 떨어진다.
따라서 ** 범용성으로 사용하다가, 세밀하게 작성해야 하면 세밀한 내용이 적용되도록 단계를 둔다**
스프링은 MessageCodesResolver 로 이러한 기능을 지원한다.
오류 코드와 메시지 처리 4#
MessageCodesResolver
- 검증 오류 코드로 코드들을 생성
- 기본 구현체는
DefaultMessageCodesResolver - 주로
ObjectError,FieldError와 사용
** DefaultMessageCodesResolver**의 기본 메시지 생성 규칙
** 객체 오류**
1.:code+","+object name
2.:codeex) 에러코드가 totalPriceMin objectName이 item이라면
totalPriceMin.item (1순위) => 없으면 totalPriceMin(2순위)
** 필드 오류**
1.:code+"."+object name + "." + field
2.:code + "." + field
3. code+ "." + field type
4. codeex) 에러코드가 required objectName이itemName이라면
- “required.item.itemName”
- “required.itemName”
- “required.java.lang.String”
- “required”
** 오류 메시지 출력 **
타임리프가 랜더링 할때 오류가 있다면 th:errors가 생성된 메시지 코드들을 순서대로 돌아가면서 메시지를 찾고 출력 (없으면 default->에러남)
오류 코드와 메시지 처리5#
정리
rejectValue()호출MessageCodesResolver를 사용해서 검증 오류 코드로 메시지 코드 생성 3.new FieldError()를 생성하면서 메시지 코드들을 보관 4.th:errors에서 순서대로 메시지를 찾음
오류 코드와 메시지 처리 6#
검증 오류 코드는 2가지로 나뉨
- 개발자가 직접 설정 ->
rejectValue()직접 호출 - 스프링이 직접 검증 오류에 추가 (주로 타입정보가 안맞음)
스프링은 타입 오류가 발생하면 typeMismatch라는 오류 코드를 생성
=> 이게 MessageCodesResolver를 통하면서 4가지 메시지 코드 생성됨
errors.properties에 메시지 코드를 정의하면 됨 ex)
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.Validator 분리#
컨트롤러에서 검증하지않고, 따로 Validator를 만들어서 검증!
스프링에서 제공하는 Validator를 구현 오버라이딩
public boolean supports(Class<?? class)return Item.class.isAssignableFrom(clazz);원하는 객체인지
- `public void validate(Object target, Errors errors)
- 검증해서 에러를 담아주는 역활
- object=객체, Errors=
bindingResult
추가적인 도움#
컨트롤러에서 WebDataBinder를 통해서 검증기를 추가하고
메소드에서 @Validated를 통해서 검증기를 실행할 수 있다.