Validation은 크게 두가지가 있다.
- JavaBean 기반 Validation (가장 많이 사용)
- Spring Validation 인터페이스 구현하여 Validation
org.springframework.validation.Validatior
애플리케이션에서 사용하는 객체 검증용 인터페이스
특징
- 어떤 계층과도 관계가 없다. -> 모든 계층(웹, 서비스, 데이터) 에서 사용해도 좋다.
- 구현체 중 하나로, JSR-303(Bean Validation 1.0), JSR-349(Bean Validation 1.1) 을 지원한다.(LocalValidatorFactoryBean)
- DataBinder에 들어가 바인딩 할 때 같이 사용되기도 한다.
인터페이스 : org.springframework.validation.Validator
- boolean support (Class clazz) : 어떤 타입의 객체를 검증할 때 사용할 것인지 결정함.
- void validate(Object obj, Errors e) : 실제 검증 로직을 이 안에서 구현
- 구현 할 때 ValidationUitils 사용하며 편리하다.
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/validation/Validator.html
Validator (Spring Framework 5.2.7.RELEASE API)
A validator for application-specific objects. This interface is totally divorced from any infrastructure or context; that is to say it is not coupled to validating only objects in the web tier, the data-access tier, or the whatever-tier. As such it is amen
docs.spring.io
public class UserLoginValidator implements Validator {
private static final int MINIMUM_PASSWORD_LENGTH = 6;
public boolean supports(Class clazz) {
return UserLogin.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
UserLogin login = (UserLogin) target;
if (login.getPassword() != null
&& login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
errors.rejectValue("password", "field.min.length",
new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
"The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
}
}
}
스프링 부트 2.0.5 이상 버전을 사용할 때
- LocalValidatorFactoryBean 빈으로 자동 등록
- JSR-380(Bean Validation 2.0.1) 구현체로 hibernate-validator 사용.
- https://beanvalidation.org/
Spring boot Validation
스프링 2.3.0 이상 버전에서 더이상 Validation을 자동으로 가져오지 않기 때문에 아래 의존성 추가를 해준다.
<!-- 이 코드를 추가해준다! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
DTO에 검증 조건 설정
public class CreateBookRequest {
@NotBlank(message = "책 이름은 필수로 입력해야합니다.")
@Size(max=50, message="최대 길이는 20입니다")
private String name;
@Min(0, "가격은 0원보다 커야합니다.")
private int price;
// ...
}
이런식으로 검증에 필요한 조건들을 넣어준다.
요청 Body로 들어오는 DTO 매개변수에 @Valid 어노테이션만 붙여서 훨씬 편리하게 사용할 수 있다.
@PostMapping("/books")
public CreateBookResponse createBook(@Valid @RequestBody CreateBookRequest) {
// ...
}
만약 검증 도중 실패하면 -> MethodArgumentNotValidException이 발생!
ExceptionHanlder를 이용해 에러를 알려주자
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
에러 발생 시:
{
"name": "책 이름은 필수로 입력해야합니다.",
"price": "가격은 0원보다 커야합니다."
}
Spring validator 인터페이스 구현을 통한 validation
- supports: 이 validator가 동작할 조건을 정의, 주로 클래스 타입을 비교
- validate: 원하는 검증을 진행한다.
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할 field(title)가 empty or whitespace 일때, errorcode로 notEmpty를 출력, defaultMessage는 errorcode를 찾지 못했을때, 출력!
}
}
package com.example.demospring51;
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;
}
}
@Component
public class AppRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
Event event = new Event();
EventValidator eventValidator = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event, "event");
//어떤 객체를 검사할것이고, 어떤 이름인지 파라미터로 전달
//우리가 테스트중이라서 BeanPropertyBindingResult 이 클래스를 직접 만들지만, 실제로는 spring MVC가 알아서 만들어서 매개변수를 전달해준다.
//평소에는 전혀 사용할 일이 없다.
event.setTitle("title");
eventValidator.validate(event,errors);
System.out.println(errors.hasErrors());
//error가 있는지?
errors.getAllErrors().forEach(e ->{
System.out.println("==== error code ====");
Arrays.stream(e.getCodes()).forEach(System.out::println);
System.out.println(e.getDefaultMessage());
});
}
}
실행 결과
true
==== error code ====
notEmpty.event.title
notEmpty.title
notEmpty.java.lang.String
notEmpty
Empty title is not allowed.
title이 empty이기 때문에, errors.hasErrors(); 가 true를 return하고,
errors에다가 에러를 담아줄것이고,
에러 코드가 순차적으로 출력이 된다.
error code를 보면, 내가 만든 errorcode외에 notEmpty.event.title / notEmpty.title / notEmpty.java.lang.String이 세가지가 추가로 출력이 됐다.(Validator에서 알아서 추가해준것이다.)
ValidationUtils를 사용하지 않고 에러를 검증하는 방법 (잘 안쓰임)
Event event = (Event)target;
if(event.getTitle() == null) {
errors.rejectValue(String field, String errorcode);
}
(특정필드에 대한 에러가 아니라)전반적인 객체의 에러일땐 -> reject(String errorcode , String defaultMessage)
객체의 특정 필드에 대한 에러 -> rejectValue(String field, String errorcode , String defaultMessage)
최근에는 Validator를 사용하지않고, 스프링 부트를 사용한다면
기본적으로 우리가 구현한 Validator중에 LocalValidatorFactoryBean을 빈으로 자동등록해준다. -> spring이
아무런 빈을 등록을 안했지만, 아래와 같이 주입받아서 validate를 사용 할 수있다.
EventValidator eventValidator = new EventValidator();
eventValidator.validate(event,errors);
위에 코드는 사용하지 않는다.
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
Validator validator;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(validator.getClass());
Event event = new Event();
event.setLimit(-1); //일부러 0이하 값을 넣는다.
event.setEmail("a2232");//이메일이 아닌 값을 넣는다.
//EventValidator eventValidator = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event, "event");
//어떤 객체를 검사할것이고, 어떤 이름인지 파라미터로 전달
//우리가 테스트중이라서 BeanPropertyBindingResult 이 클래스를 직접 만들지만, 실제로는 spring MVC가 알아서 만들어서 매개변수를 전달해준다.
//평소에는 전혀 사용할 일이 없다.
//eventValidator.validate(event,errors);
validator.validate(event, errors);
System.out.println(errors.hasErrors());
//error가 있는지?
errors.getAllErrors().forEach(e ->{
System.out.println("==== error code ====");
Arrays.stream(e.getCodes()).forEach(System.out::println);
System.out.println(e.getDefaultMessage());
});
}
}
public class Event {
Integer id;
@NotEmpty
String title;
@NotNull @Min(0)
Integer limit;
@Email
String email;
//... getter and setter....
}
각각 필드에 annotation을 붙여줬다. 그리고 title은 값을 안넣었고, limit는 -1, email에는 이메일이 아닌값을 넣어줬다.
실행 결과
class org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
true
==== error code ====
NotEmpty.event.title
NotEmpty.title
NotEmpty.java.lang.String
NotEmpty
비어 있을 수 없습니다
==== error code ====
Min.event.limit
Min.limit
Min.java.lang.Integer
Min
0 이상이어야 합니다
==== error code ====
Email.event.email
Email.email
Email.java.lang.String
Email
올바른 형식의 이메일 주소여야 합니다
맨 윗줄을 보면, LocalValidatorFactoryBean이 자동으로 주입이 되었다.
결과는, title은 @NotEmpty 를 붙여줬기 때문에 오류가 발생하였고,
limit은 @min(0) , email은 @Email 어노테이션 때문에 오류가 발생하였다.
생각해보기
복잡하게 Validation 인터페이스를 구현해서 하는 것 보다 간단하게 1차적으로 @Valid 어노테이션으로 검증하고 2차적으로 비즈니스 로직에서 검증을 하는게 좋을 것 같다.
REFERENCE
Validation은 크게 두가지가 있다.
- JavaBean 기반 Validation (가장 많이 사용)
- Spring Validation 인터페이스 구현하여 Validation
org.springframework.validation.Validatior
애플리케이션에서 사용하는 객체 검증용 인터페이스
특징
- 어떤 계층과도 관계가 없다. -> 모든 계층(웹, 서비스, 데이터) 에서 사용해도 좋다.
- 구현체 중 하나로, JSR-303(Bean Validation 1.0), JSR-349(Bean Validation 1.1) 을 지원한다.(LocalValidatorFactoryBean)
- DataBinder에 들어가 바인딩 할 때 같이 사용되기도 한다.
인터페이스 : org.springframework.validation.Validator
- boolean support (Class clazz) : 어떤 타입의 객체를 검증할 때 사용할 것인지 결정함.
- void validate(Object obj, Errors e) : 실제 검증 로직을 이 안에서 구현
- 구현 할 때 ValidationUitils 사용하며 편리하다.
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/validation/Validator.html
Validator (Spring Framework 5.2.7.RELEASE API)
A validator for application-specific objects. This interface is totally divorced from any infrastructure or context; that is to say it is not coupled to validating only objects in the web tier, the data-access tier, or the whatever-tier. As such it is amen
docs.spring.io
public class UserLoginValidator implements Validator {
private static final int MINIMUM_PASSWORD_LENGTH = 6;
public boolean supports(Class clazz) {
return UserLogin.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
UserLogin login = (UserLogin) target;
if (login.getPassword() != null
&& login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
errors.rejectValue("password", "field.min.length",
new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
"The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
}
}
}
스프링 부트 2.0.5 이상 버전을 사용할 때
- LocalValidatorFactoryBean 빈으로 자동 등록
- JSR-380(Bean Validation 2.0.1) 구현체로 hibernate-validator 사용.
- https://beanvalidation.org/
Spring boot Validation
스프링 2.3.0 이상 버전에서 더이상 Validation을 자동으로 가져오지 않기 때문에 아래 의존성 추가를 해준다.
<!-- 이 코드를 추가해준다! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
DTO에 검증 조건 설정
public class CreateBookRequest {
@NotBlank(message = "책 이름은 필수로 입력해야합니다.")
@Size(max=50, message="최대 길이는 20입니다")
private String name;
@Min(0, "가격은 0원보다 커야합니다.")
private int price;
// ...
}
이런식으로 검증에 필요한 조건들을 넣어준다.
요청 Body로 들어오는 DTO 매개변수에 @Valid 어노테이션만 붙여서 훨씬 편리하게 사용할 수 있다.
@PostMapping("/books")
public CreateBookResponse createBook(@Valid @RequestBody CreateBookRequest) {
// ...
}
만약 검증 도중 실패하면 -> MethodArgumentNotValidException이 발생!
ExceptionHanlder를 이용해 에러를 알려주자
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
에러 발생 시:
{
"name": "책 이름은 필수로 입력해야합니다.",
"price": "가격은 0원보다 커야합니다."
}
Spring validator 인터페이스 구현을 통한 validation
- supports: 이 validator가 동작할 조건을 정의, 주로 클래스 타입을 비교
- validate: 원하는 검증을 진행한다.
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할 field(title)가 empty or whitespace 일때, errorcode로 notEmpty를 출력, defaultMessage는 errorcode를 찾지 못했을때, 출력!
}
}
package com.example.demospring51;
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;
}
}
@Component
public class AppRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
Event event = new Event();
EventValidator eventValidator = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event, "event");
//어떤 객체를 검사할것이고, 어떤 이름인지 파라미터로 전달
//우리가 테스트중이라서 BeanPropertyBindingResult 이 클래스를 직접 만들지만, 실제로는 spring MVC가 알아서 만들어서 매개변수를 전달해준다.
//평소에는 전혀 사용할 일이 없다.
event.setTitle("title");
eventValidator.validate(event,errors);
System.out.println(errors.hasErrors());
//error가 있는지?
errors.getAllErrors().forEach(e ->{
System.out.println("==== error code ====");
Arrays.stream(e.getCodes()).forEach(System.out::println);
System.out.println(e.getDefaultMessage());
});
}
}
실행 결과
true
==== error code ====
notEmpty.event.title
notEmpty.title
notEmpty.java.lang.String
notEmpty
Empty title is not allowed.
title이 empty이기 때문에, errors.hasErrors(); 가 true를 return하고,
errors에다가 에러를 담아줄것이고,
에러 코드가 순차적으로 출력이 된다.
error code를 보면, 내가 만든 errorcode외에 notEmpty.event.title / notEmpty.title / notEmpty.java.lang.String이 세가지가 추가로 출력이 됐다.(Validator에서 알아서 추가해준것이다.)
ValidationUtils를 사용하지 않고 에러를 검증하는 방법 (잘 안쓰임)
Event event = (Event)target;
if(event.getTitle() == null) {
errors.rejectValue(String field, String errorcode);
}
(특정필드에 대한 에러가 아니라)전반적인 객체의 에러일땐 -> reject(String errorcode , String defaultMessage)
객체의 특정 필드에 대한 에러 -> rejectValue(String field, String errorcode , String defaultMessage)
최근에는 Validator를 사용하지않고, 스프링 부트를 사용한다면
기본적으로 우리가 구현한 Validator중에 LocalValidatorFactoryBean을 빈으로 자동등록해준다. -> spring이
아무런 빈을 등록을 안했지만, 아래와 같이 주입받아서 validate를 사용 할 수있다.
EventValidator eventValidator = new EventValidator();
eventValidator.validate(event,errors);
위에 코드는 사용하지 않는다.
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
Validator validator;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(validator.getClass());
Event event = new Event();
event.setLimit(-1); //일부러 0이하 값을 넣는다.
event.setEmail("a2232");//이메일이 아닌 값을 넣는다.
//EventValidator eventValidator = new EventValidator();
Errors errors = new BeanPropertyBindingResult(event, "event");
//어떤 객체를 검사할것이고, 어떤 이름인지 파라미터로 전달
//우리가 테스트중이라서 BeanPropertyBindingResult 이 클래스를 직접 만들지만, 실제로는 spring MVC가 알아서 만들어서 매개변수를 전달해준다.
//평소에는 전혀 사용할 일이 없다.
//eventValidator.validate(event,errors);
validator.validate(event, errors);
System.out.println(errors.hasErrors());
//error가 있는지?
errors.getAllErrors().forEach(e ->{
System.out.println("==== error code ====");
Arrays.stream(e.getCodes()).forEach(System.out::println);
System.out.println(e.getDefaultMessage());
});
}
}
public class Event {
Integer id;
@NotEmpty
String title;
@NotNull @Min(0)
Integer limit;
@Email
String email;
//... getter and setter....
}
각각 필드에 annotation을 붙여줬다. 그리고 title은 값을 안넣었고, limit는 -1, email에는 이메일이 아닌값을 넣어줬다.
실행 결과
class org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
true
==== error code ====
NotEmpty.event.title
NotEmpty.title
NotEmpty.java.lang.String
NotEmpty
비어 있을 수 없습니다
==== error code ====
Min.event.limit
Min.limit
Min.java.lang.Integer
Min
0 이상이어야 합니다
==== error code ====
Email.event.email
Email.email
Email.java.lang.String
Email
올바른 형식의 이메일 주소여야 합니다
맨 윗줄을 보면, LocalValidatorFactoryBean이 자동으로 주입이 되었다.
결과는, title은 @NotEmpty 를 붙여줬기 때문에 오류가 발생하였고,
limit은 @min(0) , email은 @Email 어노테이션 때문에 오류가 발생하였다.
생각해보기
복잡하게 Validation 인터페이스를 구현해서 하는 것 보다 간단하게 1차적으로 @Valid 어노테이션으로 검증하고 2차적으로 비즈니스 로직에서 검증을 하는게 좋을 것 같다.