Back-End/Spring

[MVC-2편] 오류 코드와 메시지 처리

COBI-98 2023. 4. 16. 18:29

해당 포스팅은 인프런스프링 MVC2편 강의를 듣고 적은 강의 노트를 정리하며 기록하기 위한 글입니다.

강의를 시청하며, 프로젝트에 적용할 수 있는 부분들이나 궁금한 기능들을 정리하며 포스팅할 예정입니다. 

 

제 개인적인 의견이 더해져 올바르지 않은 정보가 들어가 있다면, 피드백이나 댓글로 남겨주시면 감사하겠습니다.

자세한 강의 내용은 인프런 스프링 MVC에서 만나보실 수 있습니다.

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

목표

오류 메시지를 체계적으로 다뤄보자.

개발자에게 요구사항이 여러 가지로 올 수 있지만, 현재 검증 요구사항으로

상품 생성시 가격설정을 천 원 이상 백만 원 이하, 문자가 들어오면 검증 오류처리하는 요구사항이 있다고 가정한다.

 

 

필드오류 (FieldError) , 글로벌,객체 오류 (ObjectError)

ObjectError 생성자 요약

public ObjectError(String objectName, String defaultMessage) {}


특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서 bindingResult에 담아두면 된다.
objectName : @ModelAttribute 의 이름
defaultMessage : 오류 기본 메시지

 

FieldError 생성자 요약

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 : 기본 오류 메시지

FieldError , ObjectError의 생성자는 codes , arguments를 제공한다. 

이것은 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.

 

스프링 부트가 해당 메시지 파일을 인식할 수 있게 다음 설정을 추가한다. 이렇게 하면
messages.properties , errors.properties 두 파일을 모두 인식한다.

 

application.properties

spring.messages.basename=messages,errors

 

errors.properties

range.item.price=가격은 {0} ~ {1} 까지 허용합니다.

 

//range.item.price=가격은 {0} ~ {1} 까지 허용합니다.

bindingResult.addError(new FieldError("item", "price", item.getPrice(),
false, new String[]{"range.item.price"}, new Object []{1000, 1000000}, null));

 

codes : required.item.itemName 를 사용해서 메시지 코드를 지정한다. 메시지 코드는 하나가 아니라
배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
arguments : Object[]{1000, 1000000}를 사용해서 코드의 {0} , {1}로 치환할 값을 전달한다.

 

BindingResult : rejectValue() , reject()

컨트롤러에서 BindingResult 는 검증해야 할 객체인 target 바로 다음에 온다. 따라서
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)

 

reject()

void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String 
defaultMessage);

rejectValue()

void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);

field : 오류 필드명
errorCode : 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. 뒤에서 설명할
messageResolver를 위한 오류 코드이다.)
errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지

 

//range.item.price=가격은 {0} ~ {1}까지 허용합니다.

bindingResult.rejectValue("price", "range", new Object []{1000, 1000000}, null)
앞에서 BindingResult 는 어떤 객체를 대상으로 검증하는지 target을 이미 알고 있다고 했다. 

따라서 target(item)에 대한 정보는 없어도 된다. 오류 필드명은 동일하게 price를 사용했다.

 

FieldError()를 직접 다룰 때는 오류 코드를 range.item.price 와 같이 모두 입력했다. 

그런데 rejectValue() 를 사용하고부터는 오류 코드를 range로 간단하게 입력했다. 

그래도 오류 메시지를 잘 찾아서 출력한다.

무언가 규칙이 있는 것 처럼 보인다. 이 부분을 이해하려면 MessageCodesResolver를 이해해야 한다. 

 

범용적 메시지처리, MessageCodesResolver Test

오류 코드를 만들 때 다음과 같이 자세히 만들 수도 있고, range.item.price : 상품의 가격 범위 오류 입니다.
또는 다음과 같이 단순하게 만들 수도 있다. range : 범위 오류입니다.

 

코드가 있으면 이 메시지를 높은 우선순위로 사용하는 것이다.
#Level1
range.item.price : 상품의 가격 범위 오류입니다.
#Level2
range : 범위 오류입니다.

 

우선 테스트 코드로 MessageCodesResolver를 알아보자.

package hello.itemservice.validation;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;

public class MessageCodesResolverTest {


    MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();

    @Test
    void messageCodesResolverField() {
        String[] messageCodes = codesResolver.resolveMessageCodes("range", "item", "price", Integer.class);

        assertThat(messageCodes).containsExactly(
                "range.item.price",
                "range.price",
                "range.java.lang.Integer",
                "range"
        );
    }
}

MessageCodesResolver 인터페이스이고 DefaultMessageCodesResolver는 기본 구현체이다.

 

객체 오류

객체 오류의 경우 다음 순서로 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

 

동작 방식 (thymeleaf)

 

  1. rejectValue() , reject()는 내부에서 MessageCodesResolver를 사용한다.  여기에서 메시지 코드들을 생성한다.
  2. FieldError , ObjectError의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다.
  3. MessageCodesResolver를 통해서 생성된 순서대로 오류 코드를 보관한다.
  4. 타임리프 화면을 렌더링 할 때 th:errors 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지
    코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력

error 메시지

 

오류 코드 관리 전략

핵심은 구체적인 것에서! 덜 구체적인 것으로!

#==FieldError==

#Level1
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.

#Level2 - 생략

#Level3
range.java.lang.String = {0} ~ {1} 까지의 문자를 입력해주세요.
range.java.lang.Integer = {0} ~ {1} 까지의 숫자를 입력해주세요.

#Level4
range= {0} ~ {1} 범위를 허용합니다.

 메시지에 1번이 없으면 2번을 찾고, 2번이 없으면 3번을 찾는다.
이렇게 되면 만약에 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용하면 된다!