[TIL] #4. 검증1 - Validation ③

kiteB·2021년 9월 27일
0

TIL-Spring4

목록 보기
8/17
post-thumbnail

오류 코드와 메시지 처리3

오류 코드는 단순하게 만들면

  • 범용성이 좋아서 여러 곳에서 사용할 수 있다.
  • 메시지를 세밀하게 작성하기 어렵다.
required : 필수 값이다.
range : 범위 오류이다.

→ 비슷한 에러가 여러 개 발생하면 어디에서 발생한 에러인지 바로 파악하기 어렵다.

반대로 너무 자세하게 만들면

  • 메시지를 의미를 명확하게 전달할 수 있다.
  • 범용성이 떨어진다.
required.item.itemName: 상품 이름은 필수이다.
range.item.price`: 상품의 가격 범위 오류이다.

→ 너무 자세하기 때문에 모든 경우마다 만들어줘야해서 비효율적이다.

그렇다면 어떤 방법을 사용해야 할까? 🤔

가장 좋은 방법은 범용성의 수준에 따라 메시지에 단계를 두는 것이다!

범용성으로 사용하다가, 세밀하게 작성해야 하는 경우에는 세밀한 내용이 적용되도록 하자!

예를 들어 required 오류가 발생할 때,

#Level1
required.item.itemName: 상품 이름은 필수이다.

#Level2
required: 필수 값이다.

평소에는 required가 출력되도록 하다가, 특별하게 itemName 값이 채워지지 않은 경우에는
더 구체적인 required.item.itemName이 출력되도록 하는 것이다!

스프링은 MessageCodesResolver을 통해 이러한 기능을 제공한다.


오류 코드와 메시지 처리4

본격적으로 우리 프로젝트에 적용하기 전에,
테스트 코드를 통해 MessageCodesResolver에 대해 알아보자!

MessageCodesResolverTest

public class MessageCodesResolverTest {

    MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();

    @Test
    void messageCodesResolverObject() {
        //객체 오류
        String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
        assertThat(messageCodes).containsExactly("required.item", "required");
    }

    @Test
    void messageCodeResolverField() {
        //필드 오류
        String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
        assertThat(messageCodes).containsExactly(
                "required.item.itemName",
                "required.itemName",
                "required.java.lang.String",
                "required"
        );
    }
}

MessageCodesResolver

MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
  • 검증 오류 코드로 메시지 코드들을 생성한다.
  • MessageCodesResolver는 인터페이스, DefaultMessageCodesResolver는 기본 구현체
  • 주로 ObjectError, FieldError와 함께 사용한다.

실행 결과


DefaultMessageCodesResolver의 기본 메시지 생성 규칙

1. 객체 오류 ObjectError

객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code

예) 오류 코드: required, object name: item
1.: required.item
2.: required

2. 필드 오류

필드 오류의 경우 다음 순서로 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"

오류 코드와 메시지 처리5

오류 코드 관리 전략

💡 구체적인 것에서 덜 구체적인 것으로!

MessageCodesResolver구체적인 것을 먼저, 덜 구체적인 것을 나중에 만든다.
→ 이렇게 하면 메시지와 관련된 공통 전략을 편리하게 도입할 수 있다!

모든 오류 코드에 대해서 메시지를 각각 다 정의하면 너무 비효율적이기 때문에,

  • 크게 중요하지 않은 메시지 → 범용성 있는 requried 같은 메시지로 끝내고,
  • 정말 중요한 메시지 → 꼭 필요할 때 구체적으로 적어서 사용

하는 방식이 더 효과적이다!


오류 코드 전략 도입

이제 우리 프로젝트에 적용해보자!

errors.properties

#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} 까지 허용합니다.

크게 객체 오류와 필드 오류를 나누고, 범용성에 따라 레벨을 나누었다.
실행하면, 레벨 1 → 2 → 3 → 4 순으로 해당하는 메시지를 찾는다.

이렇게 되면 크게 중요하지 않은 오류 메시지기존에 정의된 것을 그냥 재활용한다!

실행 결과


Level 1 주석

Level 2, 3 주석

레벨이 낮은 것을 주석해보면 점점 범용성이 큰 메시지가 출력되는 것을 발견할 수 있다!

🔗 전체 코드 확인하기


오류 코드와 메시지 처리6

📌 검증 오류 코드 종류

검증 오류 코드는 다음과 같이 2가지로 나눌 수 있다.

  • 개발자가 직접 설정한 오류 코드rejectValue()를 직접 호출
  • 스프링이 직접 검증 오류에 추가한 경우 (주로 타입 정보가 맞지 않는 경우)

지금까지는 우리(개발자)가 직접 설정한 오류 코드에 대해 알아보았다.
이제부터는 스프링이 직접 검증 오류에 추가한 경우를 살펴보면서 메시지 코드 전략의 강점을 확인해보자!😉


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

price 필드에 문자를 입력해보면,

errors.properties에 타입 오류와 관련된 메시지 코드를 작성하지 않았기 때문에,
스프링이 생성한 기본 메시지가 출력된다.

로그를 살펴보면 다음과 같이 출력된다.

codes[typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch]

이는 스프링이 타입 오류가 발생하면 typeMismatch라는 오류 코드를 사용하기 때문이고,
이 오류 코드가 MessageCodesResolver를 통해 4가지 메시지 코드로 생성된 것이다!

하지만 일반 사용자에게 저런 오류 메시지를 출력하는 것은 너무 불친절하다!😣

따로 errors.properties에 관련 내용을 추가해주자!

errors.properties에 내용 추가

typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.

실행해보면

우리가 원하는대로 잘 출력되는 것을 확인할 수 있다!


Validator 분리1

오류 메시지와 관련된 코드는 많이 깔끔해졌지만,
검증과 관련된 코드는 아직 매우 길다.😨

검증과 관련된 코드를 ItemValidator 클래스로 별도로 분리해서 관리하자!

스프링은 검증을 체계적으로 제공하기 위해서 다음 인터페이스를 제공한다.

public interface Validator {
boolean supports(Class<?> clazz);
   void validate(Object target, Errors errors);
}
  • supports() {}: 해당 검증기를 지원하는 여부 확인
  • validate(Object target, Errors errors): 검증 대상 객체와 BindingResult

🔗 전체 코드 확인하기


Validator 분리2

스프링이 Validator 인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다.

앞에서는 검증기를 직접 불러서 사용했고, 이렇게 사용해도 되기는 하지만!
Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.


WebDataBinder를 통해서 사용하기

WebDataBinder스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.

✔ 검증기 추가

@InitBinder
public void init(WebDataBinder dataBinder) {
    log.info("init binder {}", dataBinder);
    dataBinder.addValidators(itemValidator);
}

이렇게 WebDataBinder에 검증기를 추가하면 해당 컨트롤러에서 검증기를 자동으로 적용할 수 있다.

@InitBinder은 해당 컨트롤러에만 영향을 주기 때문에, 글로벌 설정은 별도로 해야한다.

@Validated 적용

@Validated는 검증기를 실행하라는 애노테이션으로, 이 애노테이션이 붙으면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행한다.

만약 여러 검증기를 등록한다면 실행할 검증기를 구분하기 위해 위에서 언급한 supports()를 사용한다.

🔗 전체 코드 확인하기

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글