해당 포스팅은 인프런스프링 MVC2편 강의를 듣고 적은 강의 노트를 정리하며 기록하기 위한 글입니다.
강의를 시청하며, 프로젝트에 적용할 수 있는 부분들이나 궁금한 기능들을 정리하며 포스팅할 예정입니다.
제 개인적인 의견이 더해져 올바르지 않은 정보가 들어가 있다면, 피드백이나 댓글로 남겨주시면 감사하겠습니다.
자세한 강의 내용은 인프런 스프링 MVC에서 만나보실 수 있습니다.
목표
오류 메시지를 체계적으로 다뤄보자.
개발자에게 요구사항이 여러 가지로 올 수 있지만, 현재 검증 요구사항으로
상품 생성시 가격설정을 천 원 이상 백만 원 이하, 문자가 들어오면 검증 오류처리하는 요구사항이 있다고 가정한다.
필드오류 (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)
- rejectValue() , reject()는 내부에서 MessageCodesResolver를 사용한다. 여기에서 메시지 코드들을 생성한다.
- FieldError , ObjectError의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다.
- MessageCodesResolver를 통해서 생성된 순서대로 오류 코드를 보관한다.
- 타임리프 화면을 렌더링 할 때 th:errors 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지
코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력
오류 코드 관리 전략
핵심은 구체적인 것에서! 덜 구체적인 것으로!
#==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번을 찾는다.
이렇게 되면 만약에 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용하면 된다!
'Back-End > Spring' 카테고리의 다른 글
[JPA] JPA를 사용하는 이유와 JPA에 대하여 (0) | 2023.04.18 |
---|---|
[Spring] 테스트 코드 작성, Mock (0) | 2023.04.17 |
[MVC-1편] 웹페이지, 타임리프(Thymeleaf) 구성 (0) | 2023.03.22 |
[MVC-1편] given-when-then 패턴이란 (0) | 2023.03.18 |
[MVC-1편] 기본 기능, HTTP 요청, 요청 파라미터 (0) | 2023.03.14 |