일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- HttpServletResponse
- 코드트리
- 코드트리조별과제
- 오블완
- 다형성
- 코딩테스트
- 의존관계
- 싱글톤
- 추상클래스
- 참조변수
- 인터페이스
- 백준
- JSON
- 김영한
- equals()
- 티스토리챌린지
- fielderror
- @configuration
- objecterror
- 서블릿
- http 메시지 컨버터
- 테스트코드
- DI
- java
- 스프링
- html form
- 스프링컨테이너
- 오버라이딩
- 프록시
- ocp
- Today
- Total
minOS
오류 코드와 메세지 처리 본문
오류 코드와 메시지 처리1
목표 :오류 메시지를 체계적으로 다루어보자.
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` 를 제공한다. 이것은 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.
errors 메시지 파일 생성
errors.properties 만들기
required.item.itemName=상품 이름은 필수입니다. range.item.price=가격은 {0} ~ {1} 까지 허용합니다. max.item.quantity=수량은 최대 {0} 까지 허용합니다. totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
스프링 부트 메시지 설정 추가
application.propertiesspring.messages.basename=messages,errors
ValidationItemControllerV2 - addItemV3() 추가
@PostMapping("/add") //`BindingResult bindingResult` 파라미터의 위치는 `@ModelAttribute Item item` 다음에 와야 한다. public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult ,RedirectAttributes redirectAttributes, Model model) { // bindReuslt = errors 와 같은 기능 ,검증 오류 결과를 보관 //검증 로직 if(!StringUtils.hasText(item.getItemName())){ //아이템 네임의 글자가 없으면 bindingResult.addError(new FieldError("item","itemName",item.getItemName(),false,new String[]{"required.item.itemName"},null,null)); } 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,10000},null)); } if (item.getQuantity() ==null ||item.getQuantity()> 9999){ bindingResult.addError(new FieldError("item","quantity",item.getQuantity(),false,new String[]{"max.item.quantity"},new Object[]{9999},null)); } //특정 필드가 아니 복합 룰 검증 if (item.getPrice() != null && item.getQuantity()!=null){ int resultPrice = item.getPrice() * item.getQuantity(); if( resultPrice < 10000){ bindingResult.addError(new ObjectError("item",new String[]{"totalPriceMin"},new Object[]{10000,resultPrice},null)); } } // 검증에 실패하면 다른 입력 폼으로 if(bindingResult.hasErrors()){ log.info("error ={}", bindingResult); //model.addAttribute("errors",errors); BindResult는 자동으로 view로 넘어감 return "/validation/v2/addForm"; } // 성공 로직 Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/validation/v2/items/{itemId}"; }
//range.item.price=가격은 {0} ~ {1} 까지 허용합니다. new FieldError("item", "price", item.getPrice(), false, new String[] {"range.item.price"}, new Object[]{1000, 1000000}
1)codes` : `required.item.itemName` 를 사용해서 메시지 코드를 지정한다. 메시지 코드는 하나가 아니라 배 열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
2) arguments` : `Object[]{1000, 1000000}` 를 사용해서 코드의 `{0}` , `{1}` 로 치환할 값을 전달한다.
실행 결과
오류 코드와 메시지 처리2
목표 : FieldError` , `ObjectError` 는 다루기 너무 번거롭다.
오류 코드도 좀 더 자동화 할 수 있지 않을까? 예) `item.itemName` 처럼
컨트롤러에서 `BindingResult` 는 검증해야 할 객체인 `target` 바로 다음에 온다. 따라서 `BindingResult` 는 이 미 본인이 검증해야 할 객체인 `target` 을 알고 있다.
log.info("objectName={}",bindingResult.getObjectName()); log.info("target={}",bindingResult.getTarget());
로그 출력 결과
`rejectValue()` , `reject()` `BindingResult` 가 제공하는 `rejectValue()` , `reject()` 를 사용하면 `FieldError` , `ObjectError` 를 직 접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.
`rejectValue()
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
- field : 오류 필드명
- errorCode` : 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. 뒤에서 설명할 messageResolver를 위한 오류 코드이다.)
- errorArgs : 오류 메시지에서 `{0}` 을 치환하기 위한 값
- defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
ValidationItemControllerV2 - addItemV4() 추가
@PostMapping("/add") //`BindingResult bindingResult` 파라미터의 위치는 `@ModelAttribute Item item` 다음에 와야 한다. public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult ,RedirectAttributes redirectAttributes, Model model) { // bindReuslt = errors 와 같은 기능 ,검증 오류 결과를 보관 log.info("objectName={}",bindingResult.getObjectName()); log.info("target={}",bindingResult.getTarget()); /* ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required"); 아래 검증로직과 같은 의미 */ //검증 로직 if(!StringUtils.hasText(item.getItemName())){ //아이템 네임의 글자가 없으면 bindingResult.rejectValue("itemName","required"); } if (item.getPrice() ==null || item.getPrice() < 1000 || item.getPrice()>1000000){ bindingResult.rejectValue("price","range",new Object[]{1000,10000},null); } if (item.getQuantity() ==null ||item.getQuantity()> 9999){ bindingResult.rejectValue("quantity","max",new Object[]{9999},null); } //특정 필드가 아난 복합 룰 검증 if (item.getPrice() != null && item.getQuantity()!=null){ int resultPrice = item.getPrice() * item.getQuantity(); if( resultPrice < 10000){ bindingResult.reject("totalPriceMin",new Object[]{10000,resultPrice},null); } } // 검증에 실패하면 다른 입력 폼으로 if(bindingResult.hasErrors()){ log.info("error ={}", bindingResult); //model.addAttribute("errors",errors); BindResult는 자동으로 view로 넘어감 return "/validation/v2/addForm"; } // 성공 로직 Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/validation/v2/items/{itemId}"; } //@PostMapping("/add") //`BindingResult bindingResult` 파라미터의 위치는 `@ModelAttribute Item item` 다음에 와야 한다. public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult ,RedirectAttributes redirectAttributes, Model model) { itemValidator.validate(item,bindingResult); // 검증에 실패하면 다른 입력 폼으로 if(bindingResult.hasErrors()){ log.info("error ={}", bindingResult); //model.addAttribute("errors",errors); BindResult는 자동으로 view로 넘어감 return "/validation/v2/addForm"; }
addItemV3의 축약전 오류 코드
bindingResult.addError(new FieldError("item", "price",item.getPrice(),false,new String[]{"range.item.price"},new Object[]{1000,10000},null));
축약된 오류 코드
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null)
앞에서 `BindingResult` 는 어떤 객체를 대상으로 검증하는지 target을 이미 알고 있다고 했다. 따라서 target(item)에 대한 정보는 없어도 된다. 오류 필드명은 동일하게 `price` 를 사용했다.
FieldError()` 를 직접 다룰 때는 오류 코드를 `range.item.price` 와 같이 모두 입력했다. 그런데 `rejectValue()` 를 사용하고 부터는 오류 코드를 `range` 로 간단하게 입력했다. 그래도 오류 메시지를 잘 찾아서 출력한다. 무언가 규칙이 있는 것 처럼 보인다. 이 부분을 이해하려면 `MessageCodesResolver 를 이해해야 한다.
오류 코드와 메시지 처리3
오류 코드를 만들 때 다음과 같이 자세히 만들 수도 있고, 단순하게 만들 수도 있다.
자세한 버전
required.item.itemName : 상품 이름은 필수 입니다. range.item.price : 상품의 가격 범위 오류 입니다.
단순한 버전
required : 필수 값 입니다. range : 범위 오류 입니다.
단순하게 만들면 범용성이 좋아서 여러곳에서 사용할 수 있지만, 메시지를 세밀하게 작성하기 어렵다.
반대로 너무 자세 하게 만들면 범용성이 떨어진다.
가장 좋은 방법은 범용성으로 사용하다가, 세밀하게 작성해야 하는 경우에는 세밀한 내용이 적용되도록
메시지에 단계를 두는 방법이다.
예시#Level1 required.item.itemName: 상품 이름은 필수 입니다. #Level2 required: 필수 값 입니다.
이렇게 객체명과 필드명을 조합한 메시지가 있는지 우선 확인하고, 없으면 좀 더 범용적인 메시지를 선택하도록 추 가 개발을 해야겠지만, 범용성 있게 잘 개발해두면, 메시지의 추가 만으로 매우 편리하게 오류 메시지를 관리할 수 있을 것이다.
스프링은 `MessageCodesResolver` 라는 것으로 이러한 기능을 지원한다.
오류 코드와 메시지 처리4
목표 :테스트 코드로 MessageCodesResolver를 알아보자.
import org.junit.jupiter.api.Test; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; import static org.assertj.core.api.Assertions.*; public class MessageCodesResolverTest { MessageCodesResolver codesResolver = new DefaultMessageCodesResolver(); @Test void messageCodeResolverObject(){ String[] messageCodes = codesResolver.resolveMessageCodes("required", "item"); for( String messageCode : messageCodes){ System.out.println("messageCode = " + messageCode); } assertThat(messageCodes).containsExactly("required.item","required"); } @Test void messageCodesResolverField(){ String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class); for(String messageCode: messageCodes){ System.out.println("messageCode = " + messageCode); } assertThat(messageCodes).containsExactly( "required.item.itemName", "required.itemName" , "required.java.lang.String", "required" ); } }
MessageCodesResolver
1) 검증 오류 코드로 메시지 코드들을 생성한다.
2)MessageCodesResolver인터페이스이고 `DefaultMessageCodesResolver` 는 기본 구현체이다.
3) 주로 다음과 함께 사용 `ObjectError` , `FieldError`
객체 오류(ObjectError)
객체 오류의 경우 다음 순서로 2가지 생성 1.: code + "." + object name 2.: code 예) 오류 코드: required, object name: item 1.: required.item 2.: required
필드 오류(FieldError)
필드 오류의 경우 다음 순서로 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"
동작 방식
-`rejectValue()` , `reject()` 는 내부에서 `MessageCodesResolver` 를 사용한다. 여기에서 메시지 코드들을 생성한다.
- FieldError` , `ObjectError` 의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다. `
MessageCodesResolver` 를 통해서 생성된 순서대로 오류 코드를 보관한다.이 부분을 `BindingResult` 의 로그를 통해서 확인해보자.
오류 코드에 자세한 오류 부터 단순한 오류 순으로 담겨있음을 확인 할 수 있다.
*FieldError:`rejectValue("itemName", "required")` 다음 4가지 오류 코드를 자동으로 생성
`required.item.itemName` `required.itemName` `required.java.lang.String` `required`
ObjectError : reject("totalPriceMin")` 다음 2가지 오류 코드를 자동으로 생성
`totalPriceMin.item` `totalPriceMin`
오류 메시지 출력
타임리프 화면을 렌더링 할 때 `th:errors` 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지 코드를 순서대 로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다.
오류 코드와 메시지 처리5
목표 : 오류 코드 관리 전략 - 핵심은 구체적인 것에서 ~> 덜 구체적인 것으로!
MessageCodesResolver는 `required.item.itemName` 처럼 구체적인 것을 먼저 만들어주고, `required` 처
럼 덜 구체적인 것을 가장 나중에 만든다. 이렇게 하면 앞서 말한 것 처럼 메시지와 관련된 공통 전략을 편리하게 도입할 수 있다.
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} 까지 허용합니다. #스프링이 직접 검증 오류에 추가한 경우(주로 타입 정보가 맞지 않음) typeMismatch.java.lang.Integer=숫자를 입력해주세요. typeMismatch=타입 오류입니다.
크게 객체 오류와 필드 오류를 나누었다. 그리고 범용성에 따라 레벨을 나누어두었다.
itemName` 의 경우 `required` 검증 오류 메시지가 발생하면 다음 코드 순서대로 메시지가 생성된다.
1. `required.item.itemName` 2. `required.itemName` 3. `required.java.lang.String` 4. `required`
구체적인 것에서 덜 구체적인 순서대로 찾는다. 메시지에 1번이 없으면 2번을 찾고, 2번이 없으면 3번을 찾는다. 이렇게 되면 만약에 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용 하면 된다
원래 오류 메세지
Level1 전부 주석
Level2,3 전부 주석
정리
1. rejectValue() 호출2. MessageCodesResolver 를 사용해서 검증 오류 코드로 메시지 코드들을 생성
3) new FieldError()를 생성하면서 메시지 코드들을 보관
4) th:erros 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출
오류 코드와 메시지 처리6
검증 오류 코드는 다음과 같이 2가지로 나눌 수 있다.
1) 개발자가 직접 설정한 오류 코드 ->`rejectValue()` 를 직접 호출
2) 스프링이 직접 검증 오류에 추가한 경우(주로 타입 정보가 맞지 않음)
타입 오류가 나면
오류 메세지로 해당 로그를 보여준다.
#스프링이 직접 검증 오류에 추가한 경우 typeMismatch.java.lang.Integer=숫자를 입력해주세요. typeMismatch=타입 오류입니다.
이렇게 오류 메세지를 쓰고, 다시 서버를 돌리면해당 결과를 얻을 수 있다.
'TIL > 김영한의 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술' 카테고리의 다른 글
Bean Validation - Bean Validation 애노테이션 적용,스프링 적용,에러 코드 (0) | 2024.09.18 |
---|---|
Validator 분리 (1) | 2024.09.14 |
Validation - FieldError, ObjectError (4) | 2024.09.11 |
Validation - BindingResult (0) | 2024.09.11 |
Validation - 검증 직접 처리 (3) | 2024.09.11 |