Validation 이란
스프링에서는 Validator 인터페이스를 지원하여 애플리케이션에서 사용하는 객체를 검증할 수 있는 기능을 제공한다. 이 Validator 인터페이스는 어떤 특정 계층에 사용하는 기능이 아닌 모든 계층에서 사용할 수 있다.
기존에 Controller나 Service단에서 넘어오는 Param들을 체크하기 위한 로직이 들어가면서 복잡성과 가독성이 떯어짐에 따라 나온 라이브러리 같습니다. 받는 DTO Param 안에 어노테이션으로 정의하여 해당 Param이 가져야 하는 규칙들을 DTO 안에 둘 수 있어서 이로 인해 단일 책임 원칙 (Single responsibility principle)을 해치지 않을 수 있는 장점이 있는 것 같습니다.
라이브러리 등록
Gladle
implementation 'org.springframework.boot:spring-boot-starter-validation'
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation
예제
들어가기 전에 Validation은 처리하는 방법은 크게 두가지 방식이 있습니다.
1. annotaion을 이용한 지정된 Valid 처리
2. 커스텀 annotation을 생성하는 방식이 있습니다.
일단 들어가기 전에 위에 두가지 방식을 쓰기 전에 선언 부터할게요.
1. Controller 선언
아래와 같은 컨트롤러 단에 사용할 메서드에 사용할 DTO에 @Valid라고 선언을 해야만 해당 DTO에 정의된 Validation을 체크할 수 있습니다.
참고
해당 import값입니다.
import javax.validation.*;
1-1. annotation을 이용한 지정된 Valid 처리
아래와 같이 처리하고자 하는 변수 위에 Validation에서 지원해 주는 @NotBlank , @NotNull 등 을 지정하고 반환 message들을 등록하여 사용하는 방식입니다.
여러 가지 어노테이션을 중첩으로 사용하셔도 됩니다.
1-2. 커스텀 annotation을 생성
예제로는 전화번호 유효성 체크하는 예제로 만들어 보겠습니다.
1-2-1. 커스텀 annotaion 선언
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
@Documented
public @interface PhoneNumber {
String message() default "{javax.validation.constraints.PhoneNumber.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List{
PhoneNumber[] value();
}
}
위에서 선언된 message , groups, payload와 내부에 있는 List는 필수 값이에요. 꼭 작성해 주셔야 됩니다.
@Constraint에 해당 어노테이션이 체크해야 될 유효성 체크로직이 들어있는 클래스를 선업니다.
1-2-2. 유효성 체크 클래스 생성
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (phoneNumberValid(value)) {
return true;
}
return false;
}
private static boolean phoneNumberValid(String value) {
String phone = toPhoneNumber(value.replaceAll("-", ""));
if (phone.matches("^\\d{3}-\\d{3,4}-\\d{4}$")) {
return true;
} else if (phone.matches("^\\d{2,3}-\\d{3,4}-\\d{4}$")) {
return true;
} else if (phone.matches("^\\d{4}-\\d{4}$")) {
return true;
} else {
return false;
}
}
private static String toPhoneNumber(String value) {
if (value.startsWith("02")) {
return value.replaceAll("([0-9]{2})([0-9]+)([0-9]{4})", "$1-$2-$3");
} else if (value.length() == 8) {
return value.replaceAll("([0-9]{4})([0-9]{4})", "$1-$2");
} else {
return value.replaceAll("([0-9]{3})([0-9]+)([0-9]{4})", "$1-$2-$3");
}
}
}
위에 ConstraintValidator를 상속받아서 작성해 주시면 됩니다. 여기서 isValid를 override를 해서 true를 내보내면 유효성에 문제가 없는 것이고, false는 문제 발생 시 내보내게 됩니다.
1-2-3. 커스텀 Annotaion을 선언
/**
* 전화번호
* */
@NotBlank(message = "phoneNumber cannot be empty or null.")
@PhoneNumber(message = "The format of the phone number message is strange.")
private String phoneNumber;
1-1에서 사용하는 방식과 같이 선언만 해서 사용하시면 됩니다.
참고로 혹시 커스텀 어노테이션에 사용자가 지정한 사이즈라 던가 특정값을 받고 싶다면 어노테이션에 추가해서 받을 수도 있습니다.
아래에 짧은 예제입니다.
- 어노테이션 선언
package io.com.open.javaopenvalid.support;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
@Documented
public @interface Password {
String message() default "{javax.validation.constraints.Password.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 대문자 필수 포함 여부
* */
boolean capitalLetter() default true;
/**
* 소문자 필수 포함 여부
* */
boolean smallLetter() default true;
/**
* 특수 문자 필수 포함 여부
* */
boolean specialSymbol() default true;
/**
* 숫자 포함여부
* */
boolean number() default true;
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List{
Password[] value();
}
}
- 선언한 어노테이션에 값을 빼서 쓸 때
package io.com.open.javaopenvalid.support;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class PasswordValidator implements ConstraintValidator<Password, String> {
private boolean capitalLetter = true;
private boolean smallLetter = true;
private boolean specialSymbol = true;
private boolean number = true;
private int mixedNum = 0;
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (passwordCheck(value)) {
return true;
}
return false;
}
@Override
public void initialize(Password constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
this.capitalLetter = constraintAnnotation.capitalLetter();
mixedNum += this.capitalLetter ? 1 : 0;
this.smallLetter = constraintAnnotation.smallLetter();
mixedNum += this.smallLetter ? 1 : 0;
this.specialSymbol = constraintAnnotation.specialSymbol();
mixedNum += this.specialSymbol ? 1 : 0;
this.number = constraintAnnotation.number();
mixedNum += this.number ? 1 : 0;
}
...
}
initialize를 override를 받아서 뽑아 쓰시면 됩니다.
- 선언 부분
예제는 대문자는 필수로 포함 안 해도 된다는 선언입니다.
@Password(
message = "The format of the password message is strange.",
capitalLetter = false
)
private String password;
Spring이 지원해 주는 기본 Validation
참고. 기본적으로 message는 다 지원합니다. 유효성 체크 후 예외 안내 메시지 용도
- 문자 체크 : 해당 String만 체크가 정상적으로 되며, 내부적으로는 null이 아니고 빈값이 아닌 경우 체크
@NotBlank(message = "name cannot be empty or null.")
- Null 체크 : Null만 체크가 필요한 경우 사용
@NotNull(message = "age cannot be null.")
- 숫자 검증
//음수 검증 0 포함
@NegativeOrZero(message = "age negative number including zero")
//음수 검증
@Negative(message = "age negative number including zero")
// 양수 검증 0 포함
@PositiveOrZero(message = "age positive number including zero")
// 양수 검증
@Positive(message = "age positive number not including zero")
- 크기 체크 : 길이가 특정 값보다 크고 작은지 체크
@Size(
min = 8, max = 16,
message = "Password must be at least 8 digits"
)
⚬ min : 최소 사이즈
⚬ max : 최대 사이즈
- 이메일 체크 : 이메일 패턴이 맞는지 체크
@Email(message = "The format of the email message is strange."
, regexp = "^[a-zA-Z0-9+-\\_.]+@[a-zA-Z0-9-]+\\.[io]+$"
)
⚬ regexp : 체크할 정규표현식 등록
- 날짜 검증
// 오늘 보다 이후에 들어온 날짜인지 체크
@Negative(message = "오늘 날짜보다 미래가 아닙니다.")
// 오늘포함 이후에 들어온 날짜인지 체크
@NegativeOrZero(message = "오늘 날짜보다 포함 미래가 아닙니다.")
// 오늘포함 이전에 들어온 날짜인지 체크
@PositiveOrZero(message = "오늘 날짜 포함 과거가 아닙니다.")
// 오늘보다 이전에 들어온 날짜인지 체크
@Past(message = "오늘 날짜보다 과거가 아닙니다.")
Junit 테스트 방법
@SpringBootTest
class ValidRequestTest {
// 해당 부분들을 호출
private static ValidatorFactory factory;
private static Validator validator;
@BeforeAll
public static void init() {
factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
@DisplayName("name 빈문자열 전송 시 에러 발생")
void 빈문자열_유효성_실패_테스트() {
// given
ValidRequest validRequest = ValidRequest.of("홍길동", 11, "system@gmail.com", true, LocalDateTime.now().minusDays(1L), "01012341234", "asdfASDF1234!@$#");
validRequest.setName("");
// when
Set<ConstraintViolation<ValidRequest>> violations = validator.validate(validRequest);
// then
assertThat(violations).isNotEmpty();
violations.forEach(error -> {
// 예외 상황들이 이 곳으로 들어옵니다.
assertThat(error.getMessage()).isEqualTo("name cannot be empty or null.");
});
}
...
}
'Java > Spring' 카테고리의 다른 글
[Spring] Rest API 통신 방법 ( RestTemplate vs FeignClient vs WebClient ) (0) | 2023.05.05 |
---|---|
[Spring] AOP (0) | 2023.03.23 |
[Spring] MapStruct 적용 방법 (0) | 2023.03.21 |
[AWS] SQS Listener 구축 ( Java + Gradle + Spring ) (1) | 2023.03.17 |
[Java] Faker Library (2) | 2023.03.11 |