해당 포스팅에서 사용된 예제 코드는 spring-boot validation example에서 확인 가능합니다.
Custom Validator
사용 가능한 constraint 어노테이션이 제공하는 제약 조건 외에 필요한 경우, 직접 만들어서 사용할 수 있다.
이전 포스팅에서 InPutRequest
와 InputEntity
클래스에서 정규식(Regular Expression)을 사용하여 String이 유효한 PinCode 형식(6자리)인지 확인했었다. 이 부분을 별도의 Validator를 구현해 대체해보려고 한다.
먼저, custom constraint 어노테이션 PinCode
를 생성한다.
1 2 3 4 5 6 7 8 9 10 11 12
| @Target({ FIELD }) @Retention(RUNTIME) @Constraint(validatedBy = PinCodeValidator.class) @Documented public @interface PinCode {
String message() default "{PinCode.invalid}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {}; }
|
custom constraint 어노테이션에는 다음과 같은 것들이 필요하다.
- parameter
message
: ValidationMessages.properties
에서 특정 property key를 가리키는 메시지 (제약 조건 위반시 메시지로 사용된다.)
- parameter
groups
: 유효성 검사가 어떤 상황에서 실행되는지 정의할 수 있는 매개 변수 그룹.
- parameter
payload
: 유효성 검사에 전달할 payload를 정의할 수 있는 매개 변수.
@Constraint
: ConstraintValidator
interface 구현을 나타내는 어노테이션
PinCode validator는 다음과 같이 구현한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class PinCodeValidator implements ConstraintValidator<PinCode, String> {
@Override public boolean isValid(String value, ConstraintValidatorContext context) { final Pattern pattern = Pattern.compile("^[0-9]{6}$"); final Matcher matcher = pattern.matcher(value);
try { return matcher.matches(); } catch (Exception e) { return false; } } }
|
이제 다른 constraint 어노테이션과 마찬가지로 @PinCode 어노테이션을 사용할 수 있다.
1 2 3 4 5 6 7
| public class InputEntityWithCustomValidator {
@PinCode private String pinCode; }
|
직접 Validator를 생성해 Validation 하기
Spring이 지원하는 Bean Validator에 의존하지 않고 직접 Bean Validation을 하고자하는 경우가 있을 수 있다.
이 경우, 직접 Validator
를 생성하고 validation을 할 수 있다. Spring의 지원이 전혀 필요하지 않다는 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Service @RequiredArgsConstructor public class DirectlyValidateService {
public void validateInput(InputEntity inputEntity) { final ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); final Validator validator = factory.getValidator(); final Set<ConstraintViolation<InputEntity>> violations = validator.validate(inputEntity); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } } }
|
그러나, Spring Boot는 이미 사전에 설정되어 만들어진 Validator
인스턴스를 제공한다. 그렇기 때문에 위와 같이 별도의 Validator 인스턴스를 직접 생성하지 않고 서비스에서 주입받아 사용하면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Service @RequiredArgsConstructor public class DirectlyValidateService {
private final Validator validator; public void validateInputWithInjectedValidator(InputEntity inputEntity) { final Set<ConstraintViolation<InputEntity>> violations = validator.validate(inputEntity); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } } }
|
위의 Service는 Spring에 의해 Bean으로 생성될 때, Validator를 주입받게 된다.
위의 두 방법이 제대로 동작하는지 확인하기 위해 테스트 코드를 작성해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @ExtendWith(SpringExtension.class) @SpringBootTest class DirectlyValidateServiceTest {
@Autowired private DirectlyValidateService service;
@Test void whenInputEntityIsInvalid_thenThrowsException() { final InputEntity inputEntity = new InputEntity(); inputEntity.setNumberBetweenOneAndTen(50); inputEntity.setNotEmptyString(""); inputEntity.setPinCode("1234");
assertThrows(ConstraintViolationException.class, () -> { service.validateInput(inputEntity); }); }
@Test void givenInjectedValidator_whenInputEntityIsInvalid_thenThrowsException() { final InputEntity inputEntity = new InputEntity(); inputEntity.setNumberBetweenOneAndTen(50); inputEntity.setNotEmptyString(""); inputEntity.setPinCode("1234");
assertThrows(ConstraintViolationException.class, () -> { service.validateInputWithInjectedValidator(inputEntity); }); } }
|
Validation Groups
종종 특정 객체(클래스)는 서로 다른 상황에서 공유되어 사용될 수 있다.
예를 들면, CRUD와 같은 작업에서 “Create”와 “Update”를 수행할 때 같은 객체(클래스)를 사용하는 것이다. 그러나 다음의 경우처럼 서로 다른 상황에서 실행되어야 하는 validation이 있을 수 있다.
- “Create” 상황에서만 Validation
- “Update” 상황에서만 Validation
- 두 가지 상황 모두에서 Validation
위와 같이 Validation 규칙을 구현할 수 있는 Bean Validation 기능을 Validation Groups
이라고 부른다.
이전에 custom constraint 어노테이션을 직접 만들면서 groups
필드가 반드시 있어야 하는것을 보았다. 이를 이용해서 Validation이 실행되어야 하는 특정 validation group을 명시할 수 있다.
CRUD 예제를 위해 OnCreate
와 OnUpdate
라는 2개의 marker interface를 정의한다.
1 2 3
| interface OnCreate {}
interface OnUpdate {}
|
그 다음 marker interface를 다음과 같은 constraint 어노테이션과 함께 사용할 수 있다.
1 2 3 4 5 6 7 8
| class InputEntityWithCustomValidator {
@Null(groups = OnCreate.class) @NotNull(groups = OnUpdate.class) private Long id; }
|
위의 코드는 “Create” 상황에서는 id가 null일 수 있고, “Update” 상황에서는 not null이어야 함을 의미한다.
Spring은 @Validated
어노테이션을 이용해 validation group을 사용할 수 있도록 지원한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Service @Validated public class ValidateServiceWithGroups {
@Validated(OnCreate.class) void validateForCreate(@Valid InputEntityWithCustomValidator input) { }
@Validated(OnUpdate.class) void validateForUpdate(@Valid InputEntityWithCustomValidator input) { } }
|
@Validated
어노테이션은 클래스에도 적용되어야 하며, validation group에 의한 Validation을 하기 위해서는 메서드에도 적용되어야 한다.
제대로 동작하는지 확인하기 위해 테스트 코드를 작성해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @ExtendWith(SpringExtension.class) @SpringBootTest class ValidateServiceWithGroupsTest {
@Autowired private ValidateServiceWithGroups service;
@Test void whenInputIsInvalidForCreate_thenThrowsException() { InputEntityWithCustomValidator input = new InputEntityWithCustomValidator(); input.setId(17L); input.setNumberBetweenOneAndTen(5); input.setNotEmptyString("not empty"); input.setPinCode("123456");
assertThrows(ConstraintViolationException.class, () -> { service.validateForCreate(input); }); }
@Test void whenInputIsInvalidForUpdate_thenThrowsException() { InputEntityWithCustomValidator input = new InputEntityWithCustomValidator(); input.setId(null); input.setNumberBetweenOneAndTen(5); input.setNotEmptyString("not empty"); input.setPinCode("123456");
assertThrows(ConstraintViolationException.class, () -> { service.validateForUpdate(input); }); } }
|