BackEnd/Spring MVC

[Spring] 검증1 - Validation - 4

샤아이인 2022. 3. 6.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

10. 오류 코드와 메시지 처리 5

● 핵심은 구체적인 것 을 먼저 사용하고, 덜 구체적일수록 나중에 사용한다.

MessageCodesResolver 는 required.item.itemName 처럼 구체적인 것을 먼저 만들어주고, required 처럼 덜 구체적인 것을 가장 나중에 만든다.

 

그럼 왜 이런식으로 오류코드를 만들까?

모든 오류코드에 대하여 메시지를 전부 다 만들기는 힘들다.

일반적이면서 중요하지 않은 메시지는 범용적인 오류 코드인 required 와 같이 사용하고, 정말 중요한 내용은 필요할때 구체적으로 적어 사용해야하는 것 이다.

 

예를 들어 itemName의 경우 required 검증 오류 메시지가 발생하면 다음 메시지 코드를 순서대로 메시지가 생성된다.

1. required.item.itemName
2. required.itemName
3. required.java.lang.String
4. required
 

이렇게 생성된 메시지 코드를 기반으로 1번부터 순서대로 MessageSource에서 메시지를 찾는다.

 

● 정리

1. rejectValue() 호출

2. MessageCodesResolver 를 사용해서 검증 오류 코드로 메시지 코드들을 생성

3. new FieldError() 를 생성하면서 메시지 코드들을 보관

4. th:erros 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출

 

● ValidationUtils

 

- 기존 코드

if (!StringUtils.hasText(item.getItemName())) { 
    bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은 필수입니다."); 
}
 

위의 기존코드를 ValidationUtils를 사용하여 다음과 같이 변경할수 있다.

 

- ValidationUtils 사용 후

ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
 

11. 오류 코드와 메시지 처리 6

검증 오류 코드는 다음과 같이 2가지로 나눌 수 있다.

1) 개발자가 직접 설정한 오류 코드 => rejectValue() 를 직접 호출

2) 스프링이 직접 검증 오류에 추가한 경우 (주로 타입 정보가 맞지 않음)

 

예를들어 다음과 같이 price 필드에 문자열로 "문자입력" 을 전달하면 위에서 말한 2가지 case중 2번에 해당되게 된다.

하지만 빨간 경고문을 보면 너무 장황한 설명이 나오는것을 확인할 수 있다.

우리가 추가하지도 않았는데 말이다.

필드값이 바인딩이 되지 않기 때문에 스프링이 직접 BindingResult에 FieldError을 만들어서 넣어준다.

또한 다음과 같이 메시지 코드가 생성된다.

codes[typeMismatch.item.price, typeMismatch.price, typeMismatch.java.lang.Integer, typeMismatch]
 

에러 코드 부분을 보면 typeMismatch가 보인다. 이는 우리가 추가해준것이 아니라, 스프링이 추가해준 것 이다.

 

다음과 같이 4가지 메시지 코드가 입력되어 있다.

typeMismatch.item.price

typeMismatch.price

typeMismatch.java.lang.Integer

typeMismatch

 

그렇다. 스프링은 타입 오류가 발생하면 typeMismatch 라는 오류 코드를 사용한다.

이 오류 코드가 MessageCodesResolver 를 통하면서 4가지 메시지 코드가 생성된 것이다.

 

이를 우리가 원하는 문구로 출력하기 위해 다음과 같이 추가해줄 수 있다.

#추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요. 
typeMismatch=타입 오류입니다.
 

다시 실행해보면 이전과 달리 우리가 추가한 메시지가 나오는것을 확인할 수 있다.

 

12. Validator 분리1

기존의 컨트롤러 내부의 검증 로직은 너무 지저분 하다.

이를 별도의 검증 부분으로 분리하여 ItemValidator 라는 class로 만들어보자. 이러면 재사용성 또한 높일 수 있다.

 

● 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) {
        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);
            }
        }
    }
}
 

스프링이 제공하는 Validator를 구현한 구현체를 직접 만들어 사용하였다.

supports() 를 통해 해당 clazz를 지원하는지 여부를 확인한 후, validate()를 통해 검증을 진행할 수 있다!

 

● 변경된 addItemV5

private final ItemValidator 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를 보면 Spring의 Validator라는 interface를 구현하고 있다.

이렇게 구현하면 Spring이 검증과정에서 도움을 주는 부분이 있다. 이는 다음 글에서 살펴보자.

 

13. Validator 분리2

스프링이 Validator 인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다.

이전 코드에서는 검증기를 직접 DI 받아서 사용했다. 하지만 Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 도움을 받을 수 있다.

 

● WebDataBinder 를 통하여 검증하기

WebDataBinder 는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.

 

기존 컨트롤러에 다음 코드를 추가한다.

WebDataBinder에 우리가 만든 검증기를 추가해주는 코드이다.

@InitBinder
public void init(WebDataBinder dataBinder){
    dataBinder.addValidators(itemValidator);
}
 

이렇게 되면 컨트롤러가 호출될때마다 dataBinder가 새로 만들어 진 후, 항상 itemValidator를 추가하게 된다.

이렇게 WebDataBinder 에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.

@InitBinder => 해당 컨트롤러에만 영향을 준다.

 

이제 추가된 검증기를 사용하는 @Validated 를 활용해 보자.

 

● 변경된 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}";
}
 

validator를 직접 호출하는 부분이 사라지고, 대신에 검증 대상인 Item 앞에 @Validated이 붙었다. 기존과 동일하게 작동한다.

 

@Validated는 검증기를 실행하라는 애노테이션 이다.

이 애노테이션이 붙으면 앞서 WebDataBinder 에 등록한 검증기를 찾아서 실행한다.

그런데 검증기를 여러개 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports()가 사용된다.

'BackEnd > Spring MVC' 카테고리의 다른 글

[Spring] 검증2 - Bean Validation - 2  (0) 2022.03.07
[Spring] 검증2 - Bean Validation - 1  (0) 2022.03.07
[Spring] 검증1 - Validation - 3  (0) 2022.03.06
[Spring] 검증1 - Validation - 2  (0) 2022.03.06
[Spring] 검증1 - Validation - 1  (0) 2022.03.05

댓글