Java Category/Spring

[Spring MVC] 검증(Validation) - Validator 분리

ReBugs 2024. 3. 13.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


컨트롤러에서 검증 로직이 차지하는 부분은 매우 크다면,  별도의 클래스로 역할을 분리하는 것이 좋다. 그리고 이렇게 분리한 검증 로직을 재사용 할 수도 있다.

 

Validator 인터페이스

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

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

boolean supports(Class<?> clazz)

이 메서드는 Validator가 주어진 클래스의 인스턴스를 검증할 수 있는지 여부를 판단한다. 검증하려는 객체의 클래스가 이 메서드에서 true를 반환할 때만 validate 메서드를 통해 검증 로직이 실행된다.

예를 들어, UserForm 클래스의 인스턴스만 검증하려는 경우, 이 메서드는 UserForm.class에 대해 true를 반환하고, 다른 클래스 타입에 대해서는 false를 반환할 것이다.

 

void validate(Object target, Errors errors)

실제 객체를 검증하는 로직을 이 메서드에 구현한다. target 파라미터는 검증하려는 객체이며, errors 파라미터는 검증 과정에서 발견된 오류들을 저장하는 데 사용된다.

오류가 발견되면, Errors 객체에 오류 정보를 추가함으로써, 어플리케이션 나머지 부분에서 이를 처리할 수 있도록 한다.

 

예시

public class UserValidator implements Validator {
    
    @Override
    public boolean supports(Class<?> clazz) {
        return User.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        User user = (User) target;
        
        if (StringUtils.hasText(user.getName()) == false) {
            errors.rejectValue("name", "required", "이름은 필수입니다.");
        }

        // 추가적인 검증 로직 구현...
    }
}

이 예제에서 UserValidator 클래스는 Validator 인터페이스를 구현하여 User 클래스의 인스턴스를 검증한다.

supports 메서드는 User 클래스의 인스턴스에 대해서만 검증을 지원한다고 선언하고, validate 메서드는 User 객체의 name 필드가 비어 있는지 확인하는 간단한 검증 로직을 포함한다.

 

검증기

@Component 를 이용하여 해당 클래스를 스프링 빈에 등록한다.

@Component
public class ItemValidator implements Validator {
    //supports() : 해당 검증기를 지원하는 여부 확인
    @Override
    public boolean supports(Class<?> clazz) {
        return Item.class.isAssignableFrom(clazz);
    }

    //validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult
    @Override
    public void validate(Object target, Errors errors) {
        Item item = (Item) target;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");


        //void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
        //field : 오류 필드명
        //errorCode : 오류 코드(이 오류 코드는 메시지에 등록된 코드 아님. messageResolver를 위한 오류 코드)
        //errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
        //defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지

        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() >= 10000) {
            errors.rejectValue("quantity", "max", new Object[]{9999}, null);
        }

        //특정 필드 예외가 아닌 전체 예외
        //void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);

        //errorCode : 오류 코드(이 오류 코드는 메시지에 등록된 코드 아님. messageResolver를 위한 오류 코드)
        //errorArgs : 오류 메시지에서 {0} 을 치환하기 위한 값
        //defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
        if (item.getPrice() != null && item.getQuantity() != null) {
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice < 10000) {
                errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
            }
        }
    }
}

 

컨트롤러

private final ItemValidator itemValidator;
 @PostMapping("/add")
 public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
 
 	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}";
}

 

 

WebDataBinder

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

 

WebDataBinder는 스프링 프레임워크 내에서 HTTP 요청 데이터를 JavaBean 객체에 바인딩하는 역할을 하는 핵심 클래스이다. 사용자의 입력이나 쿼리 파라미터 같은 웹 요청 데이터를 자동으로 객체의 필드에 매핑하고, 필요한 타입 변환을 수행한다. 또한, Validator 인터페이스를 사용하여 바인딩된 객체의 데이터를 검증할 수 있는 기능도 제공한다.

WebDataBinder는 컨트롤러에서 @InitBinder 어노테이션과 함께 사용되어, 특정 요청을 처리하기 전에 요청 데이터를 객체에 바인딩하고 검증하는 과정을 커스터마이징할 수 있다. 예를 들어, 특정 포맷의 날짜 문자열을 Date 타입으로 변환하거나, 입력 값 검증을 위해 사용자 정의 검증기를 등록하는 것이 가능하다.

WebDataBinder의 사용은 데이터 바인딩과 검증 로직을 컨트롤러 밖으로 분리하여, 코드의 가독성과 유지보수성을 향상시키는 데 도움을 준다.

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

private final ItemValidator itemValidator;

 @PostMapping("/add")
 public String addItemV5(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {

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

 

이전 코드와 달라진 점은

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

 

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

 

  • addValidators: DataBinder에 하나 이상의 Validator 인스턴스를 추가하는 메서드이다. 이 메서드를 통해 검증기가 등록되면, 바인딩된 객체에 대한 검증 과정에서 이 검증기가 사용된다.
  • itemValidator: 검증 로직을 구현하는 커스텀 검증기의 인스턴스이다. 이 검증기는 Validator 인터페이스를 구현해야 하며, supports() 메서드를 통해 검증 가능한 객체 타입을 지정하고, validate() 메서드에서 실제 검증 로직을 구현한다.
  • @InitBinder 는 해당 컨트롤러에만 영향을 준다. 글로벌 설정은 별도로 해야한다.

 

@Validated는 검증기를 실행하라는 애노테이션이다. 이 애노테이션이 붙으면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다.

이때 Validator의 supports() 메서드가 사용된다. 여기서는 supports(Item.class)가 호출되고, 결과가 true이므로 ItemValidator의 validate() 메서드가 호출된다.

 

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

@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 를 제거해도 글로벌 설정으로 정상 동작하는 것을 확인할 수 있다.

글로벌 설정을 하면  BeanValidator가 자동 등록되지 않는다.
검증시 @Validated, @Valid 둘 다 사용 가능하다.
javax.validation.Valid를 사용하려면 build.gradle 의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Validated는 스프링 전용 검증 어노테이션이고, @Valid는 자바 표준 검증 어노테이션이다.

@Valid는 자바 표준이므로 스프링 이외의 자바 어플리케이션에서도 널리 사용될 수 있다. @Validated는 스프링에 특화된 기능(예: 검증 그룹)을 제공하기 때문에, 스프링 기반 어플리케이션에서 더 복잡한 검증 로직이 필요할 때 유리하다. 사용 상황에 따라 두 애노테이션을 적절히 선택하여 사용하면, 어플리케이션의 데이터 검증 요구사항을 효과적으로 충족시킬 수 있다.

댓글