일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스프링 데이터 JPA
- API 개발 고급
- QueryDSL
- JPQL
- JPA 활용2
- 김영한
- 일론머스크
- 로그인
- 검증 애노테이션
- 값 타입 컬렉션
- Spring Data JPA
- 예제 도메인 모델
- 벌크 연산
- 스프링MVC
- Bean Validation
- jpa 활용
- 임베디드 타입
- 스프링
- 트위터
- 실무활용
- 스프링 mvc
- 타임리프
- JPA
- 컬렉션 조회 최적화
- 페이징
- 프로젝트 환경설정
- 타임리프 문법
- 기본문법
- 불변 객체
- JPA 활용 2
- Today
- Total
RE-Heat 개발자 일지
스프링 MVC 2편 - [4] 검증 1 - Validation(하편) 본문
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
인프런 김영한 님의 스프링 MVC 2편 강의를 토대로 정리한 내용입니다.
[7] 오류 코드와 메시지 처리 1
오류 메시지가 일관적이지 않으면 사용자에게 혼란을 야기할 수 있다. 그리고 스프링에선 상편에서 나온 messages.properties처럼 오류 메시지를 구분하기 쉽게 관리할 수 있다.
errors.properties
① 에러 메시지를 따로 관리할 파일을 만든다.
② application.properties에
spring.messages.basename=messages, errors
을 추가해 messages.properties, errors.properties 두 파일을 모두 인식하게 만든다.(생략하면 messages.properties만 기본으로 인식한다.)
ValidationItemControllerV2 - addItemV3
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
if (!StringUtils.hasText(item.getItemName())) { //상품명 누락
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName", "required.default"}, 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, 1000000}, 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));
}
}
■ FiledError (상품명 누락·가격 입력값 오류 등)
- objectName : "item"
- field : "itemName"
- rejectedValue : item.getItemName()
- bindingFailure : 타입오류 같은 바인딩 실패인지, 검증실패인지 구분값 => false
- codes : 메시지 코드 => new String[]{"required.item.itemName", "required.default"}
▶ required.item.ItemName을 찾지 못하면 required.default를 찾아 오류 메시지로 보낸다.
- arguments : 메시지에서 사용하는 인자 => null
▶ 가격 범위가 필요할 땐 range.item.price의 {0} {1}에 들어갈 인자값을 넣는다.
- defaultMessage : 기본오류 메시지 => null (메시지 코드로 대체)
■ ObjectError
- objectName : "item"
- codes : 메시지 코드 => new String[]{"totalPriceMin"}
- arguments : 메시지에서 사용하는 인자 => new Object[]{10000, resultPrice}
- defaultMessage : 기본오류 메시지 => null
실행화면 1
실행화면 2
required.item.itemName을 찾을 수 없으면 required.default를 찾는 것을 확인할 수 있다.
둘 다 존재하지 않으면 400번 오류 메시지로 넘어간다.
[8] 오류 코드와 메시지 처리 2
목표 : FiledError와 ObjectError의 생성자에 넣어야 할 파라미터 값이 많아 번거롭다. 더 간결하게 쓸 수 있지 않을까?
BindingResult는 검증해야 할 객체인 target의 바로 다음에 온다. 그러므로 굳이 타깃까지 적어줄 필요가 없다.
ValidationItemControllerV2-addItemV3
//@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
=> log를 보면 bindingResult가 검증해야 할 객체를 알고 있다는 것을 알 수 있다.
그래서 스프링은 위 사실을 바탕으로 FiledError, ObjectError를 직접 생성하지 않고 깔끔하게 검증 오류를 다룰 수 있는 rejectValue()와 reject()를 제공한다.
ValidationItemControllerV2-addItemV4
@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
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, 1000000}, 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("errors={}" + bindingResult);
return "validation/v2/addForm";
}
■ rejectValue() :FieldError 대체
field : 오류 필드명 => "price"
errorCode : => "range" MessageCodeResolver가 range.item.price를 불러온다.
errorArgs : 오류메시지 {0} {1}을 치환하기 위한 값 => new Object[]{1000, 1000000}
defaultMessage : 오류메시지를 찾을 수 없을 때 사용하는 기본 메시지 => null
■ reject() : ObjectError 대체
errorCode : => "totalPriceMin" errors.properties의 totalPriceMin을 불러온다.
errorArgs : 오류메시지 {0} {1}을 치환하기 위한 값 => new Object[]{10000, resultPrice}
defaultMessage : 오류메시지를 찾을 수 없을 때 사용하는 기본 메시지 => null
[9] 오류 코드와 메시지 처리 3
범용성 좋은 오류 코드와 세밀한 코드 필요할 때 쓰는 방법이 있을까?
=> 가장 좋은 방법은 범용성 높은 코드를 사용하다 자세하게 작성해야 할 때 세밀한 내용이 적용되도록 메시지에 단계를 두는 방법이다.
errors.properties 예시
#Level 1
required.item.itemName: 상품이름은 필수입니다.
#Level 2
required : 필수 값입니다.
이렇게 단계별로 적용하는 기능을 스프링은 MessageCodesResolver를 통해 지원한다.
[10] 오류 코드와 메시지 처리 4
MessageCodesRewolverTest
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
void messageCodesResolverObject(){
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
assertThat(messageCodes).containsExactly("required.item", "required");
}
@Test
void messageCodesResolverField(){
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
assertThat(messageCodes).containsExactly(
"required.item.itemName",
"required.itemName",
"required.java.lang.String",
"required"
);
}
}
① MessageCodesResolver는 인터페이스, DefaultMessageCodesResolver는 구현체
주로 ObjectError, FieldError와 함께 사용한다.
■ DefaultMessageCodesResolver 기본 메시지 생성 규칙
4가지 순서로 코드 생성
- 필드 오류 [FieldError] : rejectValue("itemName", "required")
1. code + "." + object name + "." + field => required.item.itemName
2. code + "." + field => required.itemName
3. code + "." + field type => required.java.lang.String [타입 확인, 보통 스프링이 자동 생성]
4. code => required
- 객체 오류 [ObjectError] : reject("totalPriceMin")
1. code + "." + object name => totalPriceMin.item
2. code => totalPriceMin
동작방식
① rejectValue(), reject()는 내부에서 MessageCodesResolver를 사용한다. 여기에서 메시지 코드를 생성
② FieldError, ObjectError 생성자를 확인하면 오류 코드를 여러 개(new String[]{"1", "2"}) 가질 수 있다.
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName", "required.default"}, null, null));
그래서 MessageCodesResolver를 통해서 순서대로 오류 코드를 보관한다.
③ 그리고 th:errors에서 순서대로 오류 코드를 찾으므로 세밀한 순서대로 즉, 단계별로 적용할 수 있다.
[11] 오류 코드와 메시지 처리 5
모든 오류마다 메시지를 일일이 많드는 건 번거로운 일이다. 그래서 중요하지 않은 오류엔 범용성 있는 메시지, 정말 중요한 메시지는 구체적인 메시지를 보여주면 된다.
예시]
▶ 레벨 1
▶ 레벨 3
▶ 레벨 4
■ ValidationUtils
Empty, 공백 같은 단순한 기능만 제공한다.
[12] 오류 코드와 메시지 처리 6
검증 오류 코드
① 개발자가 직접 설정한 오류 코드 rejectValue()를 직접 호출
② 스프링이 직접 검증 오류 추가(주로 타입 정보가 맞지 않을 때 사용)
입력 타입을 잘못 넣었을 때 로그를 확인하면 BindingResult의 FiledError가 담겨 있고 typeMismatch라는 오류코드가 생성된 것을 확인할 수 있다.
스프링은 타입 오류가 발생하면 typeMistmatch라는 오류 코드를 사용한다. 하지만, 그 코드가 상당히 개발자(?)스럽다.
그래서 사용자 친화적인 오류 메시지를 보내려면 errors.properties에 다음 내용을 추가하면 된다.
#추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
[13] Validator 분리 1
목표 : 복잡한 검증 로직을 별도로 분리하자.
ItemValidator
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) { //Errors는 BindingResult의 부모 클래스
Item item = (Item) target;
if (!StringUtils.hasText(item.getItemName())) { //상품명 누락
errors.rejectValue("itemName", "required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
}
① Spring이 제공하는 interface인 Validator를 상속받아 사용
② 메소드 설명
supports(Class<?> claxx) : 해당 검증기를 지원하는지 여부 확인
validate(Object target, Errors errors) : 타깃은 검증대상, Erros는 BindingResult의 부모 클래스
ValidationItemControllerV2 - addItemV5() : ItemValidator 직접 호출
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증 로직
itemValidator.validate(item, bindingResult);
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("errors={}" + bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
itemValidator를 스프링 빈으로 주입받아서 직접 호출했다.
[14] Validator 분리 2
■ WebDataBinder를 이용하는 방법도 있다.
ValidationItemControllerV2
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(itemValidator);
}
① WebDataBinder에 itemValidator외 다른 검증기도 추가할 수 있다.
② @InitBinder는 해당 컨트롤러에만 영향을 주며, 해당 컨트롤러가 호출되면 제일 먼저 @InitBinder가 붙은 init메소드가 호출된다.
ValidationItemControllerV2 - addItemV6
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
//검증에 실패하면 다시 입력 폼으로
if (bindingResult.hasErrors()) {
log.info("errors={}" + bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
① 직접 호출 대신 검증기를 실행하라는 @Validated를 넣는다.
1] 이 애노테이션이 붙으면 WebDataBinder에 등록된 검증기를 찾아 실행하고 파라미터 바인딩을 해준다.
2] 검증기가 여러 개면 어떤 검증기를 사용할지 구분이 필요하고 여기서 앞서 나왔던 supports()가 사용된다.
supports(Item.class)가 호출되고 결괏값이 true이므로 ItemValidator의 validate()가 호출된다.
글로벌 설정 - 모든 컨트롤러에 적용
ItemServiceApplication
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
이러면 모든 컨트롤러에 다 적용할 수 있다. 단, 다음 챕터에서 쓰는 BeanValidator 파트에서 자동등록이 되지 않아 문제가 생기므로 제외
'백엔드 > 스프링' 카테고리의 다른 글
스프링 MVC 2편 - [5] 검증2- Bean Validation(하편) (0) | 2023.07.19 |
---|---|
스프링 MVC 2편 - [5] 검증2- Bean Validation(상편) (0) | 2023.07.19 |
스프링 MVC 2편 - [4] 검증 1 - Validation(상편) (0) | 2023.07.14 |
스프링 MVC 2편 - [3] 메시지·국제화 (0) | 2023.07.13 |
스프링 MVC 2편 - [2] 스프링 통합과 폼 (0) | 2023.07.12 |