오봉이와 함께하는 개발 블로그

스프링 MVC 2 - Validator 분리 본문

BE/Spring

스프링 MVC 2 - Validator 분리

오봉봉이 2022. 8. 23. 18:47
728x90

Validator 분리 1

컨트롤러에 있는 검증 로직을 별도로 분리하는 것이 유지보수 측면에서 더 좋을 것이다.
또 분리를 하면 검증 로직을 재사용 할 수도 있다.

ItemValidator

@Component
public class ItemValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
        // Item == clazz(파라미터)
        // Item == subItem(자식 클래스)
    }

    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;

        // errors는 BindingResult의 부모 클래스
        // 검증 로직
        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, 10000000}, null);
        }
        if(item.getQuantity() == null || item.getQuantity() > 9999) {
            errors.rejectValue("quantity", "max", new Object[]{1000}, 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);
            }
        }
    }
}

스프링을 검증을 체계적으로 제공하기 위해 인터페이스를 제공한다.

public interface Validator {
    boolean supports(Class<?> clazz);
    void validate(Object target, Errors errors);
}

supports() {} : 해당 검증기를 지원하는 여부 확인
validate(Object target, Errors errors) : 검증 대상 객체(Item)와 BindingResult(Errors의 자식 클래스)

ValidationItemControllerV2 - addItemV5()

@Controller
@RequestMapping("/validation/v2/items")
@RequiredArgsConstructor
@Slf4j
public class ValidationItemControllerV2 {

private final ItemRepository itemRepository;
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}";
    }
    // 나머지 코드들
}

Validator 분리 2

스프링이 Validator 인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다.
위에서는 검증기를 직접 불러서 사용했지만(물론 이렇게 사용해도 된다.) Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.

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

ValidationItemControllerV2

@InitBinder
public void init(WebDataBinder webDataBinder) {
    log.info("init binder {}", webDataBinder);
        webDataBinder.addValidators(itemValidator);
}

@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}";
}

WebDataBinder에 검증기를 추가하면 해당 컨트롤러가 호출될 때 자동으로 검증기를 돌려줘 자동으로 적용되며 요청이 올 때 마다 새로 만들어지는 객체다.
@InitBinder는 해당 컨트롤러에만 영향을 주기 때문에 글로벌 설정은 별도로 해야한다.

addItemV6에는 validator를 직접 호출하는 부분이 사라지고, 대신에 검증 대상 앞에 @Validated가 붙었다.

동작 방식
@Validated는 검증기를 실행하라는 어노테이션이다.
이 어노테이션이 붙으면 WebDataBinder에 등록한 검증기를 찾아서 실행한다.
그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요한데, 이 때 supports()가 사용된다.
여기서는 supports(Item.class)호출되고, 결과가 true이므로 ItemValidatorvalidate()가 호출된다.

글로벌 설정 - 모든 컨트롤러에 다 적용

@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(ItemServiceApplication.class, args);
    }

    @Override
    public Validator getValidator() {
        return new ItemValidator();
    }
}

이렇게 글로벌 설정을 추가할 수 있다.
기존 컨트롤러의 @InitBinder를 제거해도 글로벌 설정으로 정상 동작하는 것을 확인할 수 있다.
참고로 이런 글로벌 설정을 사용하는 경우는 드물다.

참고

검증시 @Validated @Valid둘 다 사용 가능하다.
javax.validation.@Valid를 사용하기 위해서는 build.gradle 의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Validated는 스프링 전용 검증 어노테이션이고, @Valid는 자바 표준 검증 어노테이션이다.

출처 : 인프런 김영한 지식공유자님 강의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술
728x90
Comments