2023.06.29 Spring
스프링 인프런 김영한
43. 오류 코드와 메시지 처리1
오류가 나면 메시지를 직접넣어줫는데
상품이름을 일관적으로 관리하고 싶다.
시스템이 커지면 일일히 맞추기가 어렵다.
오류메시지도 일관적으로 관리하는 방법이 있다.
생성자에 codes, argument도 넘길 수 있다.
파라미터 목록
objectName : 오류가 발생한 객체 이름
field : 오류 필드
rejectedValue : 사용자가 입력한 값(거절된 값)
bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값
codes : 메시지 코드
arguments : 메시지에서 사용하는 인자
defaultMessage : 기본 오류 메시지
FieldError , ObjectError 의 생성자는 codes , arguments 를 제공한다.
이것은 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.
이걸사용하면 국제화코드에서 사용햇던것 처럼 사용할 수 있다.
errors 메시지 파일 생성
messages.properties 를 사용해도 되지만
오류 메시지를 구분하기 쉽게 errors.properties 라는 별도의 파일로 관리해보자.
먼저 스프링 부트가 해당 메시지 파일을 인식할 수 있게 다음 설정을 추가한다.
이렇게하면 messages.properties , errors.properties 두 파일을 모두 인식한다. (생략하면 messages.properties 를 기본으로 인식한다.)
spring.messages.basename=messages,errorserrors.properties에 메시지를 정의해두자.
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}43.1 컨트롤러 변경
codes부분에 String배열로 코드를 넣어주면된다.
왜 배열이냐면 하나가 없으면 다음 코드 값을 보여주는 형식이다.
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[] {"required.item.itemName"}, null, "상품 이름은 필수입니다."));
}argument는 Object배열로 넘기면된다. {0번째 아규먼트} 이런식으로 넣어둔것에 알아서 들어가게 된다.
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
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} 로 치환할 값을 전달한다.
실행해보면 메시지, 국제화에서 학습한 MessageSource 를 찾아서 메시지를 조회하는 것을 확인할 수 있다
44. 오류 코드와 메시지 처리2
목표
FieldError , ObjectError 는 다루기 너무 번거롭다.
오류 코드도 좀 더 자동화 할 수 있지 않을까? 예) item.itemName 처럼?
컨트롤러에서 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)rejectValue(), reject()
BindingResult 가 제공하는 rejectValue(), reject() 를 사용하면 FieldError , ObjectError 를 직접 생성하지 않고,
깔끔하게 검증 오류를 다룰 수 있다.
rejectValue() , reject() 를 사용해서 기존 코드를 단순화해보자
44.1 컨트롤러 변경
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required");
}필드명과 errorCode를 받는다. errorCode는 맨앞만 넣으면된다.
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}argument는 여전히 Object배열로 넣어주면된다.
실행
오류 메시지가 정상 출력된다.
그런데 errors.properties 에 있는 코드를 직접 입력하지 않았는데 어떻게 된 것일까?
44.2 rejectValue()
결국에는 우리가 작성햇던 fieldError객체 등을 알아서 작성해준다.
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);field : 오류 필드명
errorCode : 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. 뒤에서 설명할 messageResolver를 위한 오류 코드이다.)
errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null)앞에서 BindingResult 는 어떤 객체를 대상으로 검증하는지 target을 이미 알고 있다고 했다.
따라서 target(item)에 대한 정보는 없어도 된다.
오류 필드명은 동일하게 price 를 사용했다.
44.3 축약된 오류 코드
FieldError() 를 직접 다룰 때는 오류 코드를 range.item.price 와 같이 모두 입력했다.
그런데 rejectValue() 를 사용하고 부터는 오류 코드를 range 로 간단하게 입력했다.
그래도 오류 메시지를 잘 찾아서 출력한다.
무언가 규칙이 있는 것 처럼 보인다. 이 부분을 이해하려면 MessageCodesResolver 를 이해해야 한다.
왜 이런식으로 오류 코드를 구성하는지 바로 다음에 자세히 알아보자.
errors.properties
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.44.4 reject()
void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);앞의 내용과 같다.
45. 오류 코드와 메시지 처리3
어떤식으로 오류코드를 설계할것인가에 대한 내용이다.
오류 코드를 만들 때 다음과 같이 자세히 만들 수도 있고,
required.item.itemName : 상품 이름은 필수 입니다.
range.item.price : 상품의 가격 범위 오류 입니다.
또는 다음과 같이 단순하게 만들 수도 있다.
required : 필수 값 입니다.
range : 범위 오류 입니다.
단순하게 만들면 범용성이 좋아서 여러곳에서 사용할 수 있지만, 메시지를 세밀하게 작성하기 어렵다.
반대로 너무 자세하게 만들면 범용성이 떨어진다.
가장 좋은 방법은 범용성으로 사용하다가, 세밀하게 작성해야 하는 경우에는 세밀한 내용이 적용되도록 메시지에 단계를 두는 방법이다.
예를 들어서 required 라고 오류 코드를 사용한다고 가정해보자.
다음과 같이 required 라는 메시지만 있으면 이 메시지를 선택해서 사용하는 것이다.
required: 필수 값 입니다.
그런데 오류 메시지에 required.item.itemName 와 같이 객체명과 필드명을 조합한 세밀한 메시지 ㅜ코드가 있으면 이 메시지를 높은 우선순위로 사용하는 것이다.
#Level1
required.item.itemName: 상품 이름은 필수 입니다.
#Level2
required: 필수 값 입니다.field에러에서 파라미터로 순서대로 정해놓고 이사면 위에것이 나오게 구현하도록 할 수 있다.
개발자는 만들어놓고 메시지만 하나씩 추가하면 전체 메시지를 관리할 수 있게 되는 것이다.
물론 이렇게 객체명과 필드명을 조합한 메시지가 있는지 우선 확인하고, 없으면 좀 더 범용적인 메시지를 선택하도록 추가 개발을 해야겠지만,
범용성 있게 잘 개발해두면, 메시지의 추가 만으로 매우 편리하게 오류 메시지를 관리할 수 있을 것이다.
스프링은 MessageCodesResolver 라는 것으로 이러한 기능을 지원한다
46. 오류 코드와 메시지 처리4
우선 테스트 코드로 MessageCodesResolver를 알아보자
MessageCodesResolver
검증 오류 코드로 메시지 코드들을 생성한다.
MessageCodesResolver 인터페이스이고 DefaultMessageCodesResolver 는 기본 구현체이다.
주로 다음과 함께 사용 ObjectError , FieldError