네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

스프링은 애플리케이션 전 계층에서 도메인 객체를 검증할 수 있는 인터페이스를 제공한다. 스프링의 bean validation을 통해 controller의 파라미터를 비즈니스 로직을 추가하지 않고 검증할 수 있는지 알아보자.

 

interface Validator

Spring은 도메인 객체를 검증할 수 있도록 Validator 인터페이스를 도입했다. Validator 인터페이스는 객체를 검증하는데 실패하면 Errors 객체에 에러를 등록한다.

Validator 인터페이스는 아래의 두가지 메서드를 가지고 있다.

- supports(Class): 매개변수로 전달된 클래스를 검증할 수 있는지 여부를 반환

- validate(Object, org.springframework.validation.Errors): 매개변수로 전달된 객체를 검증하고 실패하면 Errors 객체에 에러를 등록한다.

 

아래 코드는 Person 객체가 어떤 식으로 Validator 인터페이스를 구현하는지 보여준다.

@Getter @Setter
public class Person {
    private String name;
    private int age;
}
public class PersonValidator implements Validator {

    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }
    
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negative.value");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.old");
        }
    }
}

validate 함수를 보면, 검증에 실패한 경우 Errors 객체의 rejectValue 함수를 호출하는 것을 볼 수 있다.

rejectValue의 파라미터는 필드이름, 에러코드로 구성된다.

ValidationUtils 클래스를 이용하여 필드 검증을 하는데, 값이 비어있거나 공백문자가 있는 경우를 쉽게 확인할 수 있다.

 

위 코드를 테스트 해보자.

@Autowired
private PersonValidator personValidator;

@GetMapping("/person/validate")
public boolean directlyValidatePerson(@ModelAttribute Person person, BindingResult result) {
    logger.debug("validate directly. {}", person);
    
    personValidator.validate(person, result);
    
    return !result.hasErrors();
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = { WebAppConfig.class, ValidatorConfig.class })
public class PersonValidateControllerTest {
    
    @Autowired 
    private PersonValidator personValidator;
    
    private MockMvc mockMvc;
    
    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new PersonValidateController(personValidator)).build();
    }
    
    @Test
    public void directlyValidateEmptyNameTest() throws Exception {
        this.mockMvc.perform(get("/person/validate")
                             .param("name", "")
                             .param("age", "25")
                     .andDo(print())
                     .andExpect(status().isOk())
                     .andExpect(content().string("false"));
    }
    
    @Test
	public void directlyValidateWrongAgeTest() throws Exception {
		// 음수 나이
		this.mockMvc.perform(get("/person/validate")
				.param("name", "test")
				.param("age", "-1"))
			.andDo(print())
			.andExpect(status().isOk())
			.andExpect(content().string("false"));

		// 100살을 초과하는 나이
		this.mockMvc.perform(get("/person/validate")
				.param("name", "test")
				.param("age", "101"))
			.andDo(print())
			.andExpect(status().isOk())
			.andExpect(content().string("false"));
	}
}

 

directlyValidatePerson 메서드를 보면 파라미터에 Person 객체 외에 BindingResult 객체가 있다.

BindingResult 객체는 모델의 검증 작업에서 나타난 에러를 저장하는 역할을 하며, 이를 이용해 메서드 실행을 분기할 수 있다.

 

MessageSource

위 Validator의 validate 함수 내에서 검증 실패시 reject 함수를 호출하는 것을 살펴봤다. 이 때 reject 함수 파라미터 중 에러코드를 지정했는데, 이는 에러메시지와 관련이 있다.

에러 메시지는 보통 messages.properties와 같은 properties 파일에서 읽어오도록 구현한다. Spring에서는 MessageSource를 이용해 properties 파일로부터 에러 메시지를 가져오도록 할 수 있다. 

 

ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");

위 코드에서 name.empty는 에러코드로 messages.properties 파일에 존재하는 키 값이다.

messages.properties에는 다음과 같이 선언이 되어있다.

name.empty=The name is empty

이 값을 가져오기 위해 MessageSource를 사용하는데, MessageSource의 구현에는 두가지 방법이 있다.

- StaticMessageSource: 코드로 메시지를 등록한다.

- ResourceBundleMessageSource: 리소스 파일로부터 읽어와 등록한다.

 

properties 파일을 읽어와 에러 메시지를 읽어올 것이므로 ResourceBundleMessageSource 클래스를 이용하자.

@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
    resourceBundleMessageSource.setBasename("messages");
    resourceBundleMessageSource.setDefaultEncoding("UTF-8");
    
    return resourceBundleMessageSource;
}
@Autowired
private MessageSource messageSource;

@GetMapping("/person/validate")
public List<String> validateAndGetErrorMessages(@ModelAttribute Person person, BindingResult result) {
    logger.debug("validated. {}", person);
    
    personValidator.validate(person, result);
    
    return result.getFieldErrors().stream()
                            .map(e -> messageSource.getMessage(e, Locale.getDefault()))
                            .collect(Collectors.toList());
}
@Test
public void validateAndGetBindingResultTest() throws Exception {
    mockMvc.perform(get("/person/validate")
                    .param("name", "")
                    .param("age", "99")
           .andDo(print())
           .andExpect(status().isOk());
}

이 테스트에 대한 결과는 아래와 같다.

MockHttpServletResponse:
               Status = 200
        Error message = null
              Headers = [Content-type: "application/json;charset=UTF-8"]
         Content Type = application/json;charset=UTF-8
                 Body = ["The name is empty"]
        Forwarded URL = null
       Redirected URL = null
              Cookies = []

 

Spring Validation

Spring 3 부터 Bean Validation API를 제공한다. Spring Bean Validation은 Validator 인터페이스를 이용하여 직접 도메인 객체를 검증하는 방법을 표준화하고 어노테이션을 이용해 표현할 수 있도록 도와준다.

Bean Validation 명세는 구현체로 Hibernate Validator를 지원한다.

Hibernate Validator는 자주 쓰이는 몇가지 검증 어노테이션을 built-in으로 제공한다.

@Getter @Setter
public class Car {

    @NotBlank(message = "The manufacturer must not be empty.")
    private String manufacturer;
    
    @Range(min = 0, max = 10, message = "The seat count must be between 0 ~ 10")
    private int seatCount;
    
    @Range(min = 0, max = 300, message = "The speed must be between 0 ~ 300")
    private int topSpeed;
}

세가지 필드를 가지고 있는 간단한 도메인 클래스이다. 검증 조건을 어노테이션으로 선언하여 표현하였다. 이를 검증하는 Validator를 가지고 와야하는데 Bean Validation API에서 LocalValidatorFactoryBean 객체를 기본으로 제공해줘 이를 이용해 Validator 인터페이스의 bean을 생성할 수 있다. 이 객체는 org.springframework.validation.Validator 인터페이스 뿐만 아니라, javax.validation.ValidatorFactory, javax.validation.Validator 인터페이스를 모두 구현하고 있다. 이 bean을 이용해 애플리케이션 전 계층에서 객체를 검증할 수 있다. 아래는 Validator를 등록하는 과정이다.

@Bean
public Validator jsrValidator() {
    return new LocalValidatorFactoryBean();
}

이 Validator를 통해 Car 객체를 검증하는 컨트롤러 메서드를 작성하자.

@Autowired
@Qualifier("jsrValidator")
private Validator validator;

@GetMapping("/car/validate")
public boolean directlyValidateCar(@ModelAttribute Car car, BindingResult result) {
    logger.debug("validate directly. {}", car);
    
    validator.validate(car, result);
    logger.debug("errors: {}", result.getFiledErrors());
    
    return !result.hasErrors();
}
@Test
public void directlyValidateTest() throws Exception {
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "kook")
                        .param("seatCount", "4")
                        .param("topSpeed", "200"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("true"));
}

@Test
public void directlyValidateInvalidParamTest() throws Exception {
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "kook")
                        .param("seatCount", "4")
                        .param("topSpeed", "301"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("false"));
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "kook")
                        .param("seatCount", "-1")
                        .param("topSpeed", "301"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("false"));
    this.mockMvc.perform(get("/car/validate")
                        .characterEncoding("utf-8")
                        .param("manufacturer", "")
                        .param("seatCount", "4")
                        .param("topSpeed", "301"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string("false"));
}

위 코드에서 검증 후, 검증 에러의 로그를 찍도록 했는데 Car 클래스의 검증 어노테이션의 message 속성으로 정의한 에러메시지가 표시된다.

 

Spring MVC 3 Validation

Spring3부터 Spring MVC 컨트롤러 메서드의 파라미터를 자동으로 검증하는 어노테이션의 @Valid를 제공해준다. 지금까지는 직접 도메인 객체를 검증하는 방법이었지만, @Valid 어노테이션을 사용하면 검증하는 로직이 없어도 자동으로 검증해준다.

@Autowired
private MessageSource messageSource;

@GetMapping("/validate")
public boolean automaticallyValidateCar(@ModelAttribute @Valid Car car) {
    logger.debug("validate automatically. {}", car);
    
    return true;
}

@ExceptionHandler({BindException.class})
public ResponseEntity<String> paramValidateError(BindException ex) {
    logger.error(ex.getMessage());
    return ResponseEntity.badRequest()
                         .body(messageResource.getMessage(ex.getFielderror(), Locale.getDefault()));
}

위 코드에서 컨트롤러 메서드에서 BindingResult 객체를 받지 않는 것을 볼 수 있다. @Valid 애노테이션을 붙인 컨트롤러 메서드도 마찬가지로 검증 에러를 BindingResult 객체에 저장을 하는데, 만약 컨트롤러 메서드가 BindingResult 객체를 받지 않는다면 검증에러 발생시, BindException을 내보낸다.

이 예외는 HTTP 상태코드 400으로 처리되는데, 위 코드에서는 BindException을 400으로 처리는 그대로 하지만, body에 에러메시지를 담아서 내보내기 위해 ExceptionHandler 메서드를 정의하였다.

 

 

 

 

 

 

 출처 

https://lazymankook.tistory.com/86

 

Spring Validation

스프링은 애플리케이션 전 계층에서 도메인 객체를 검증할 수 있는 인터페이스를 제공한다. 이번 글에서는 spring의 bean validation을 통해 어떻게 controller의 파라미터를 controller에 비즈니스 로직을

lazymankook.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !