Written by
Sunwoo Han
on
on
검증
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 공부하고 정리하는 포스트입니다.
검증
검증 직접 처리
- 사용자가 상품 등록 페이지에 접근(HTTP GET /add)
- 사용자가 상품 정보를 입력 후 서버로 전송(HTTP POST /add)
- 상품이 성공적으로 등록된 후 Location 정보로 상품 정보 상세 경로를 Redirect로 응답
- 클라이언트에선 응답 받은 정보에 있는 Location 정보로 Redirect하여 신규 상세 페이지로 이동
- 사용자가 상품 등록 페이지에 접근(HTTP GET /add)
- 사용자가 상품 정보를 입력 후 서버로 전송(HTTP POST /add)
- 상품의 유효성 검증이 실패하며 검증 오류 결과가 포함된 정보를 담아 다시 상품 등록 페이지로 이동
검증에 실패하는 경우?
Null
TypeMissMatch
- 비즈니스 요구사항에 맞지 않음
다양한 검증 방식
Map
- 서버에서 전달받은 데이터를 직접 검증하여
Map
에 담아RedirectAttributes
에 담아 보내는 방법- 검증 시 오류를
errors
와 같은 이름의Map
에 저장 - 어떤 필드에서 발생한 오류인지 구분하기 위해 오류가 발생한 필드명을
key
로 사용
- 검증 시 오류를
- 타입 오류 처리가 안되는 단점
- 숫자 필드는
Integer
이므로 문자 타입으로 설정하는 것이 불가능 - MVC에서 컨트롤러에 진입하기 전에 예외 발생
- 숫자 필드는
BindingResult
- 스프링에서 제공하는 검증 오류 처리 방법
- 컨트롤러의 매핑 메서드에서 타입 불일치에 대한 대응 가능
public FieldError(String objectName, String field, String defaultMessage) {}
- 필드 오류가 있으면
FieldError
객체를 생성해bindingResult
에 담아둠objectName
:@ModelAttribute
이름field
: 오류가 발생한 필드 이름defaultMessage
: 오류 기본 메시지
public ObjectError(String objectName, String defaultMessage) {}
-
특정 필드를 넘어서는 글로벌 오류의 경우
ObjectError
객체를 이용 - 타임리프에서는
BindingResult
를 활용해 검증 오류를 표현하는 기능을 제공#field
:BindingResult
가 제공하는 검증 오류에 접근 가능th:errors
: 해당 필드에 오류가 있는 경우 태그 출력.th:if
의 편의 버전th:errorclass
:th:field
에서 지정한 필드에 오류가 있으면 class 정보를 추가
BindingResult
는 스프링이 제공하는 검증 오류를 보관하는 객체- 검증 오류 발생 시 여기에 보관
BindingResult
가 있으면@ModelAttribute
에 바인딩 시 타입 오류가 발생해도 컨트롤러가 호출됨Model
에 자동으로 포함
- 하지만, 오류가 발생하는 경우 사용자가 입력한 내용이 모두 사라짐
FieldError, ObjectError
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) {}
objectName
: 오류가 발생한 객체 이름field
: 오류 필드rejectedValue
: 사용자가 입력한 값(거절된 값)bindingFailure
: 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값codes
: 메시지 코드arguments
: 메시지에서 사용하는 인자defaultMessage
: 기본 오류 메시지
new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다.");
-
위와 같은 방식을 사용하면, 오류 발생 시 사용자 입력 값이 유지됨
- 오류가 발생한 경우 사용자의 입력 값을 보관하는 별도의 방법이 필요 -> 보관한 사용자 입력 값을 검증 오류 발생 시 화면에 다시 출력해야 함
rejectedValue
가 바로 입력 값을 저장하는 필드bindingFailure
는 타입 오류 같은 바인딩이 실패했는지 여부를 작성 -> 실패한 것이 아니면false
를 사용
오류 코드와 메시지 처리
FieldError, ObjectError
- Spring Boot가 오류 메시지를 구분하기 쉽게 파일로 만드는 방법
- 메시지 파일을 인식할 수 있도록 다음 설정을 추가
- application.properties
spring.messages.basename=messages,errors
- src/main/resources/errors.properties
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
bindingResult
코드 변경
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[] {"required.item.itemName"}, null, null));
codes
:required.item.itemName
을 사용해서 메시지 코드를 지정- 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용됨
-
arguments
:Object[] {1000, 100000}
을 사용해서 코드의{0}
,{1}
로 치환할 값을 전달 - message의 이름을 매번 적어야해서 번거롭고, 담아야 할 속성도 많아 불편함
->BindingResult
의rejectValue()
,reject()
를 사용
rejectValue(), reject()
bindingResult.rejectValue("itemName", "required");
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
errors.properties
에 있는 코드를 입력하지 않았는데 어떻게 가져오는가?
rejectValue()
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
field
: 오류 필드명errorCode
: 오류 코드 (메시지에 등록된 코드가 아니라MessageCodesResolver
를 위한 오류 코드)errorArgs
: 오류 메시지에서{0}
을 치환하기 위한 값defaultMessage
: 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
MessageCodesResolver
- 스프링에서 제공하는 마커 인터페이스인
MessageCodesResolver
에는 다음과 같은 메서드가 정의되어 있음
public interface MessageCodesResolver {
String[] resolveMessageCodes(String errorCode, String objectName);
String[] resolveMessageCodes(String errorCode, String objectName, String field, @Nullable Class<?> fieldType);
}
-
기본 구현체인
DefaultMessageCodesResolver
를 제공 -> 각종 메시지에 대한 대처가 쉬움 MessageCodesResolver
는 메시지의 단계에 따라 범용성이 낮은 순서에서 높은 순서로 차례대로 찾으면서 처음 매칭되는 결과를 가져옴errors.properties
에 다음과 같이 작성되어 있으면, 리졸버는 디테일한 순서부터 찾음
#level 1
required.item.itemName : 상품 이름은 필수입니다.
#level 2
required : 필수 값 입니다.
- 객체 오류
- 객체 오류의 경우 다음 순서로 2가지 생성
code + "." + object name
code
- ex) 오류 코드 : required, object name : item
required.item
required
- 객체 오류의 경우 다음 순서로 2가지 생성
- 필드 오류
- 필드 오류의 경우 다음 순서로 4가지 생성
code + "." + object name
code + "." + field
code + "." + field type
code
- ex) 오류 코드 : typeMismatch, object name “user”, field “age”, field type: int
"typeMismatch.user.age"
"typeMismatch.age"
"typeMismatch.int"
"typeMismatch"
- 필드 오류의 경우 다음 순서로 4가지 생성
스프링이 직접 만든 오류 메시지 처리
- 직접 정의한 오류 코드는
rejectValue()
를 직접 호출해서 담아줌 -
타입 정보 불일치와 같이 스프링이 직접 검증 오류에 추가한 경우 -> 사용자에게 노출해선 안되는 메시지까지 노출
BindingResult
를 확인하면FieldError
에 다음과 같은 메시지 코드가 생성되어 추가되어있음- typeMismatch.item.price
- typeMismatch.price
- typeMismatch.java.lang.Integer
- typeMismatch
- 기본 메시지를 사용자에게 노출하지 않기 위해
errors.properties
에 메시지를 선언
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
Validator 분리
- 스프링에서 검증에 필요한 로직을
Validator
라는 인터페이스로 정의 Xxx.class.isAssignableFrom(clazz)
: 해당 Validator 구현체는 Xxx 클래스에 대한 검증을 수행할 수 있음을 의미Errors errors
: 매개 변수 타입인 Errors는BindingResult
클래스의 부모 타입이기 때문에 공변성이 성립
애노테이션 기반 분리
Validation
인터페이스를 구현해 검증 로직을 만들면 추가적으로 애노테이션으로 검증 가능 -> 스프링의 파라미터 바인딩의 역할 및 검증 기능도 내부에 포함하는 클래스인WebDataBinder
를 이용@InitBinder
: 해당 컨트롤러에만 영향을 줌
@Validated, @Valid
-
@Validated
는 스프링 전용 검증 애노테이션이고,@Valid
는 자바 표준 검증 애노테이션 @Validated
애노테이션을 사용해서 Item의 검증 로직을 수행- 이 애노테이션이 붙으면
WebDataBinder
에 등록한 검증기를 찾아서 실행 - 여러 검증기를 등록한다면 그 중 어떤 검증기가 실행되어야 할지 구분이 필요한데, 이때
supports()
가 사용
- 이 애노테이션이 붙으면
javax.validation.@Valid
를 사용하려면 build.gradle에 의존관계 추가가 필요
implementation 'org.springframework.boot:spring-boot-starter-validation'
Comments
SPRING 의 다른 글
-
스프링 타입 컨터버 24 Jun 2022
-
API 예외 처리 17 Jun 2022
-
예외 처리와 오류 페이지 12 Jun 2022
-
로그인 처리 - 인터셉터 08 Jun 2022
-
로그인 처리 - 필터 06 Jun 2022
-
로그인 처리 - 쿠키, 세션 31 May 2022
-
Bean Validation 22 May 2022
-
검증 22 May 2022
-
메시지, 국제화 21 May 2022
-
타임리프 - 스프링 통합과 폼 19 May 2022
-
타임리프 - 기본 기능 10 May 2022
-
스프링 MVC 기본 기능 - 웹 페이지 만들기 02 May 2022
-
스프링 MVC 기본 기능 - HTTP 응답 30 Apr 2022
-
스프링 MVC 기본 기능 - HTTP 요청 24 Apr 2022
-
스프링 MVC 기본 기능 - 요청 매핑 19 Apr 2022
-
스프링 MVC 기본 기능 19 Apr 2022
-
스프링 MVC 구조 이해 14 Apr 2022
-
MVC 프레임워크 만들기 - V4, V5 12 Apr 2022
-
MVC 프레임워크 만들기 - V1, V2, V3 09 Apr 2022
-
서블릿, JSP, MVC 패턴 05 Apr 2022
-
서블릿 29 Mar 2022
-
웹 애플리케이션 이해 24 Mar 2022
-
스프링 웹 계층이란? 05 Nov 2021
-
스프링 시큐리티 공식문서 번역 27 Sep 2021
-
스프링 AOP 총정리 : 개념, 프록시 기반 AOP, @AOP 27 Apr 2021
-
SpEL (스프링 Expression Language) 25 Apr 2021
-
데이터 바인딩 추상화 : Converter와 Formatter 21 Apr 2021
-
데이터 바인딩 추상화 : PropertyEditor 12 Apr 2021
-
Validation 추상화 10 Apr 2021
-
Resource 추상화 08 Apr 2021
-
IoC 컨테이너 9부 07 Apr 2021
-
IoC 컨테이너 8부 06 Apr 2021
-
IoC 컨테이너 7부 02 Apr 2021
-
IoC 컨테이너 6부 29 Mar 2021
-
IoC 컨테이너 5부 27 Mar 2021
-
IoC 컨테이너 4부 23 Mar 2021
-
IoC 컨테이너 3부 20 Mar 2021
-
IoC 컨테이너 2부 18 Mar 2021
-
IoC 컨테이너 1부 12 Mar 2021
-
스프링 PSA 07 Jan 2021
-
스프링 @AOP 실습 07 Jan 2021
-
프록시 패턴 06 Jan 2021
-
스프링 AOP 04 Jan 2021
-
의존성 주입(Dependency Injection) 04 Jan 2021
-
스프링 빈(Bean) 02 Jan 2021
-
스프링 IoC 컨테이너 01 Jan 2021
-
스프링 IoC 01 Jan 2021