스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 공부하고 정리하는 포스트입니다.


검증

검증 직접 처리


  1. 사용자가 상품 등록 페이지에 접근(HTTP GET /add)
  2. 사용자가 상품 정보를 입력 후 서버로 전송(HTTP POST /add)
  3. 상품이 성공적으로 등록된 후 Location 정보로 상품 정보 상세 경로를 Redirect로 응답
  4. 클라이언트에선 응답 받은 정보에 있는 Location 정보로 Redirect하여 신규 상세 페이지로 이동


  1. 사용자가 상품 등록 페이지에 접근(HTTP GET /add)
  2. 사용자가 상품 정보를 입력 후 서버로 전송(HTTP POST /add)
  3. 상품의 유효성 검증이 실패하며 검증 오류 결과가 포함된 정보를 담아 다시 상품 등록 페이지로 이동

검증에 실패하는 경우?

  • 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의 이름을 매번 적어야해서 번거롭고, 담아야 할 속성도 많아 불편함
    -> BindingResultrejectValue(), 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가지 생성
      1. code + "." + object name
      2. code
    • ex) 오류 코드 : required, object name : item
      1. required.item
      2. required
  • 필드 오류
    • 필드 오류의 경우 다음 순서로 4가지 생성
      1. code + "." + object name
      2. code + "." + field
      3. code + "." + field type
      4. code
    • ex) 오류 코드 : typeMismatch, object name “user”, field “age”, field type: int
      1. "typeMismatch.user.age"
      2. "typeMismatch.age"
      3. "typeMismatch.int"
      4. "typeMismatch"

스프링이 직접 만든 오류 메시지 처리

  • 직접 정의한 오류 코드는 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'