[Spring] @Valid와 @Validated를 사용한 유효성 검증
이전글에서 @Valid와 @Validated를 사용하여 유효성 검증을 진행하였다.
검증할 객체의 변수에 javax.validation.constraints에 있는 어노테이션을 선언하여 검증을 진행하였다. 제공되는 어노테이션으로 기본적인 체크는 되겠지만 DB를 조회하여 체크하거나 별도의 로직을 체크해야 하는 상황이라면 @Valid와 @Validated만으로는 유효성 검증을 할 수 없을 것이다. 이러한 경우에는 @Valid와 @Validated를 어떻게 사용할 수 있는지 정리해 보겠다.
javax.validation.constraints에 있는 어노테이션(@NotBlank, @Min 등)을 확인해 보면 message, groups, payload메서드가 존재한다. 따라서 우리가 생성하는 어노테이션에도 message, groups, payload는 필수적이어야 한다.
@Retention(RUNTIME)
@Target({TYPE})
@Constraint(validatedBy = CustomClassConstraintValidator.class)
@Repeatable(CustomValidator.List.class)
public @interface CustomValidator {
// required
String message() default "검증 오류가 발생하였습니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// optional
Class<?>[] customValidator();
@Target({TYPE})
@Retention(RUNTIME)
public @interface List {
CustomValidator[] value();
}
}
Repeatable
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
Class<? extends Annotation> value();
}
@Repeatable은 value 값을 받도록 되어 있다. 여기서 value는 어노테이션 타입이어야 한다.
value에 들어갈 어노테이션 타입은 중복해서 사용할 어노테이션을 묶을 일종의 컨테이너 어노테이션을 의미한다.
javax.validation의 ConstraintValidator 인터페이스를 구현해주어야 한다
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) { }
boolean isValid(T value, ConstraintValidatorContext context);
}
ConstraintValidatorContext
@Component
@RequiredArgsConstructor
public class CustomClassConstraintValidator implements ConstraintValidator<CustomValidator, Object> {
private Class<?>[] customValidator;
private final ApplicationContext applicationContext;
@Override
public void initialize(CustomValidator cv) {
customValidator = cv.customValidator();
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context) {
boolean bValid = true;
try {
for (Class<?> cls : customValidator) {
ICustomValidator validator = (ICustomValidator) applicationContext.getBean(cls);
String errMessage = "";
boolean bSuccess = validator.validate(obj, errMessage);
if (!bSuccess) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(errMessage)
.addConstraintViolation();
bValid = false;
}
}
} catch (Exception e) {
context.disableDefaultConstraintViolation();
bValid = false;
}
return bValid;
}
}
initialize
ConstraintValidator인터페이스에 initialize() 메서드는 default이기 때문에 구현하지 않아도 된다.
생성한 어노테이션에 message, groups, payload 이외에 Class<?>[] customValidator를 추가한 것이 있다.
해당값을 초기화해 준다.
isValid
구현할 Validator들은 ICustomValidator를 상속받아 사용할 것이다.
인터페이스를 선언하여 context에서 bean을 가지고 온 이유는 validate라는 메서드 포맷을 정의하기 위해서다.
CustomValidator(생성한 어노테이션)에 @Constraint(validatedBy = CustomClassConstraintValidator.class) 선언하였고 해당 내용은 CustomClassConstraintValidator객체를 사용하여 검증한다는 뜻이었다.
따라서, CustomClassConstraintValidator의 isValid가 호출될 것이고 거기서 연결된 Validator들의 validate() 메서드가 검증하게 될 것이다.
public interface ICustomValidator {
boolean validate(Object obj, String message) throws Exception;
}
@Component
@RequiredArgsConstructor
public class UserCreateValidator implements ICustomValidator{
@Override
public boolean validate(Object obj, String message) throws Exception {
UserDto userDto = (UserDto) obj;
//email 필수값 체크
if(StringUtils.isEmpty(userDto.getEmail())) {
message = "[UserCreateValidatr] - 이메일은 필수값입니다."
return false;
}
return true;
}
}
CustomClassConstraintValidator객체의 isValid메서드에서 validate 메서드를 호출
@Component
@RequiredArgsConstructor
public class UserUpdateValidator implements ICustomValidator{
@Override
public boolean validate(Object obj, String message) throws CustomException {
UserDto userDto = (UserDto) obj;
//userId 필수값 체크
if(userDto.getUserId() <= 0) {
message = "[UserUpdateValidatr] - 사용자ID는 필수값입니다."
return false;
}
return true;
}
}
@Getter
@Setter
@NoArgsConstructor
@CustomValidator(customValidator = UserCreateValidator.class, groups = ValidatorGroup.CreateGroup.class)
@CustomValidator(customValidator = UserUpdateValidator.class, groups = ValidatorGroup.UpdateGroup.class)
public class UserDto {
private long userId;
private String email;
@NotBlank
private String password;
@NotBlank
private String username;
}
검증객체에 CustomValidator(생성한 어노테이션)을 선언하고 validator와 groups을 지정한다.
동일한 어노테이션을 중복해서 선언할 수 있는 이유는 @Repeatable 어노테이션은 선언해 놨기 때문이다.
@Slf4j
@RequestMapping("/devlog/user")
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping(value = "/join", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseVo<String> join(@RequestBody @Valid UserDto userDto) throws CustomException{
User user = userService.join(userDto);
return ResponseVo.success(user);
}
@PostMapping(value = "/modify", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseVo<User> modify(@RequestBody @Valid UserDto userDto) throws CustomException{
User user = userService.modify(userDto);
return ResponseVo.success(user);
}
}
검증객체(UserDto)에 password, username변수에 @NotNull 어노테이션을 선언했다.
따라서, 컨트롤러 계층에서 @Valid 어노테이션에 의해 검증될 것이다.
@Service
@RequiredArgsConstructor
@Validated
public class UserService {
@Validated({ValidatorGroup.CreateGroup.class})
public User join(@Valid UserDto userDto) {
...
}
@Validated({ValidatorGroup.UpdateGroup.class})
public User modify(@Valid UserDto userDto) {
...
}
}
다른 계층에서 @Valid 어노테이션을 사용하려면 클래스에 @Validated를 붙여주고, 유효성을 검증할 메서드의 파라미터에 @Valid를 붙여주면 유효성 검증이 진행된다.
검증 그룹은 @Validated어노테이션을 설정하여 groups를 지정하면 된다.
join 메서드를 호출할 때 UserCreateValidator가 호출되어 검증을 할 것이고 modify 메서드를 호출할 때 UserUpdateValidator가 호출되어 검증할 것이다.
테스트
message에 출력된 결과로 UserCreateValidator가 호출된 것을 확인할 수 있다.
message에 출력된 결과로 UserUpdateValidator가 호출된 것을 확인할 수 있다.
[Spring] @Valid와 @Validated를 사용한 유효성 검증 (1) | 2024.11.27 |
---|
댓글 영역