[Spring] Validation 추상화

2021. 4. 22. 00:23 Spring Framework/Spring Core

 

[Spring] Validation 추상화

org.springframework.validation.Validator는 spring에서 제공하는 객체 검증용 인터페이스이다.

주로 spring mvc에서 사용하지만 web 계층 전용 개념은 아니며 일반적으로 사용될 수 있다.

Validator는 Java EE 표준 스펙 중 하나인 Bean Validation을 지원하여 Bean Validation이 제공하는 여러 애노테이션들을 사용해서 객체 데이터를 검증할 수 있다.

 

1. Validator 인터페이스

Validator 인터페이스에는 두 가지 메소드가 정의되어있다.

boolean supports(Class<?> clazz) {
    // 인스턴스가 검증 대상 타입인지 확인
}
void validate(Object target, Errors errors) {
    // 실질적인 검증 작업
}

 

2. Validator 구현 방법

검증 대상 클래스 Event를 만든다.

public class Event {
    
    Integer id;
    
    String title;
 
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
}

 

Event 인스턴스는 title 필드가 null이면 안된다고 가정하자.

 

다음으로 Event에 대한 validation을 처리할 Validator를 구현한다.

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
 
public class EventValidator implements Validator {
 
    @Override
    public boolean supports(Class<?> clazz) {
        return Event.class.equals(clazz);
    }
 
    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "notempty", "Empty title is not allowed");
    }
}

 

validate()는 ValidationUtils를 사용해서 구현할 수 있다.

rejectIfEmptyOrWhitespace() 메소드를 사용하고 파라미터에 Errors 객체, 필드명, 에러코드, 디폴트 메시지를 전달한다.

세번째 파라미터인 에러코드는 메시지를 가져올 때 사용할 key가 된다.

지정한 에러코드에 해당하는 메시지를 못찾을 경우 디폴트 메시지를 사용한다.

 

이제 구현한 Validator를 이용해 validation을 실행해보자.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
 
import java.util.Arrays;
 
@Component
public class AppRunner implements ApplicationRunner {
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 에러를 발생시키기 위해 title 설정 안함
        Event event = new Event();
 
        // 검증할 객체 event를 전달하여 Errors 인스턴스 생성
        Errors errors = new BeanPropertyBindingResult(event, "event");
 
        // Validation 실행
        EventValidator eventValidator = new EventValidator();
        eventValidator.validate(event, errors);
 
        // 에러가 있는지
        System.out.println("hasErrors(): " + errors.hasErrors());
 
        // 발생한 에러를 순차적으로 순회하며 에러코드와 default message 출력
        errors.getAllErrors().forEach( e -> {
            System.out.println("=== Error Code ===");
            Arrays.stream(e.getCodes()).forEach(System.out::println);
            System.out.println(e.getDefaultMessage());
        });
    }
}

 

검증 대상 Event 객체를 생성한다.

에러를 발생시켜야 하므로 title 필드는 세팅하지 않는다.

 

검증할 객체 event를 전달하여 Errors 인스턴스를 생성한다.

Errors 인스턴스를 생성할때 사용한 구현체 BeanPropertyBindingResult는 spring mvc를 사용하면 자동으로 생성해서 파라미터를 전달하므로 실제로 이 구현체를 직접 사용할 경우는 거의 없다.

 

구현한 Validator 클래스 EventValidator의 객체를 생성하고 validate() 메소드로 검증을 실행한다.

검증할 객체 event와 위에서 생성한 errors를 넘겨서 호출한다.

validate() 실행 이후 이 errors를 이용해 발생한 에러를 다룰 수 있다.

 

예제 코드에서 사용된 hasErrors()는 에러가 있는지 확인하는데 사용되고 getAllErrors()는 발생한 모든 에러 객체를 가져온다.

forEach문으로 발생한 에러들을 순차적으로 순회하면서 에러코드를 출력하게 하였다.

마지막으로 getDefaultMessage()로 디폴트 메시지를 출력한다.

 

실행 결과

Validator를 구현할때 에러 코드를 'notempty'만 설정했지만 Validator에서 자동으로 notempty.event.title, notempty.title 등의 구체적인 에러 코드를 생성해주었다.

이렇게 자동으로 에러코드를 생성해주므로 Validator 구현 시 에러코드는 prefix만 설정해주면 된다.

이 에러코드는 MessageSource에서 실제 에러메시지를 읽어오는데 사용된다.

 

validate()를 구현할때 ValidationUtils만 사용해야 하는 것은 아니다.

다음은 ValidationUtils를 사용하지 않은 코드이다.

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
 
public class EventValidator implements Validator {
 
    @Override
    public boolean supports(Class<?> clazz) {
        return Event.class.equals(clazz);
    }
 
    @Override
    public void validate(Object target, Errors errors) {
        Event event = (Event) target;
        if (event.getTitle() == null) {
            errors.rejectValue("title", "notempty", "Empty title is not allowed");
        }
    }
}

 

이렇게 ValidationUtils의 rejectIfEmptyOrWhitespace() 대신 Event의 reject()를 사용해도 된다.

 

실행 결과

실행 결과는 동일하다.

 

3. LocalValidatorFactoryBean

Spring 프로젝트에서는 Validator를 직접 구현하기보다는 spring이 제공해주는 LocalValidatorFactoryBean을 사용하는데 이 클래스는 Java EE의 Bean Validation 애노테이션들을 지원한다. 특히 Spring Boot를 사용한다면 기본적으로 이 빈을 자동으로 등록해준다.

 

위에서 작성한 Event 클래스 객체를 직접 구현한 Validator가 아닌 LocalValidatorFactoryBean을 사용해서 검증해보자.

 

먼저 이 객체로 검증을 사용하려면 Bean Validation 애노테이션을 붙여줘야한다.

Validation 대상인 Event 클래스에 다음과 같이 애노테이션을 붙인다.

import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
 
public class Event {
 
    Integer id;
 
    @NotEmpty
    String title;
 
    @Min(0)
    Integer limit;
 
    @Email
    String email;
 
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
}

 

@NotEmpty는 빈 값 여부, @Min은 최소값, @Email은 이메일 주소 형식을 validate하는데 사용된다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
 
import java.util.Arrays;
 
@Component
public class AppRunner implements ApplicationRunner {
 
 
    @Autowired
    LocalValidatorFactoryBean validatorFactoryBean;
 
    @Override
    public void run(ApplicationArguments args) throws Exception {
        Event event = new Event();
        event.setLimit(-1);
        event.setEmail("1234");
 
        // 검증할 객체 event를 전달하여 Errors 인스턴스 생성
        Errors errors = new BeanPropertyBindingResult(event, "event");
 
        // Validation 실행
        validatorFactoryBean.validate(event, errors);
 
        // 에러가 있는지
        System.out.println("hasErrors(): " + errors.hasErrors());
 
        // 발생한 에러를 순차적으로 순회하며 에러코드와 default message 출력
        errors.getAllErrors().forEach( e -> {
            System.out.println("=== Error Code ===");
            Arrays.stream(e.getCodes()).forEach(System.out::println);
            System.out.println(e.getDefaultMessage());
        });
    }
}

 

Spring boot에서 제공해주는 LocalValidatorFactoryBean을 주입받는다.

Event 인스턴스를 생성하고 에러를 의도적으로 발생시키기 위해 값을 세팅하였다.

직접 구현한 Validator를 사용하는 대신 LocalValidatorFactoryBean 객체로 validate()을 실행한다.

 

실행 결과

title, limit, email 세 필드에서 에러가 발생했고 각 에러마다 LocalValidatorFactoryBean이 제공해준 에러 코드와 디폴트 메시지가 출력되었다.

이렇게 LocalValidatorFactoryBean은 에러 코드와 디폴트 메시지를 자동으로 제공해준다.

 

이와 같이 Bean Validation 애노테이션으로 검증할 수 있는 것들은 Validator를 직접 구현하지 않고도 LocalValidatorFactoryBean을 사용해서 validation 처리를 할 수 있다.

이 외에 복잡한 비즈니스 로직으로 validation 해야하는 경우에 Validator를 구현한다.

 

References

인프런 - 백기선님의 스프링 프레임워크 핵심 기술

 

출처 : atoz-develop.tistory.com/entry/Spring-Validation-%EC%B6%94%EC%83%81%ED%99%94?category=869243