상세 컨텐츠

본문 제목

Custom Annotation 만들어 직접 유효성 검사하기 (@Valid, @Validated 심화)

Spring/유효성검증

by Chan.94 2024. 11. 28. 09:24

본문

반응형

Intro

[Spring] @Valid와 @Validated를 사용한 유효성 검증

 

[Spring] @Valid와 @Validated를 사용한 유효성 검증

Intro개발을 하다 보면 DTO 또는 객체를 검증해야 하는 경우가 있다. 이럴 때 사용할 수 있는 @Valid와 @Validated 어노테이션이 존재한다. @Valid와 @Validated 어노테이션을 사용하기 위해 아래 의존성을

fvor001.tistory.com

 

이전글에서 @Valid와 @Validated를 사용하여 유효성 검증을 진행하였다.

검증할 객체의 변수에 javax.validation.constraints에 있는 어노테이션을 선언하여 검증을 진행하였다. 제공되는 어노테이션으로 기본적인 체크는 되겠지만 DB를 조회하여 체크하거나 별도의 로직을 체크해야 하는 상황이라면 @Valid와 @Validated만으로는 유효성 검증을 할 수 없을 것이다. 이러한 경우에는 @Valid와 @Validated를 어떻게 사용할 수 있는지 정리해 보겠다.


Custom Annotation 생성

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();
    }
}

 

  • @Target(TYPE)
    해당 어노테이션은 (클래스, 인터페이스, enum)에 선언 가능함
  • @Retention(RUNTIME)
    해당 어노테이션이 유지되는 시간은 런타임
  • @Constraint(validatedBy = CustomClassConstraintValidator.class)
    CustomClassConstraintValidator 객체를 통해 유효성 검사를 진행
  • @Repeatable(CustomValidator.List.class)
    동일한 어노테이션을 같은 곳에 중복해서 사용 가능하게 만들어주는 어노테이션
  • message
    유효하지 않을 경우 반환할 메세지
  • groups
    유효성 검증이 진행될 그룹
  • payload
    유효성 검증 시에 전달할 메타 정보

Repeatable

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    Class<? extends Annotation> value();
}

@Repeatable은 value 값을 받도록 되어 있다. 여기서 value는 어노테이션 타입이어야 한다.
value에 들어갈 어노테이션 타입은 중복해서 사용할 어노테이션을 묶을 일종의 컨테이너 어노테이션을 의미한다.

 

Validator 구현 (CustomClassConstraintValidator.java)

javax.validation의 ConstraintValidator 인터페이스를 구현해주어야 한다

public interface ConstraintValidator<A extends Annotation, T> {
    default void initialize(A constraintAnnotation) { }
    boolean isValid(T value, ConstraintValidatorContext context);
}
  • initialize
    Validator를 초기화하기 위한 메서드
  • isValid
    유효성을 검증하는 메서드

ConstraintValidatorContext

  • disableDefaultConstraintViolation()
    기본 메시지를 비활성화
  • buildConstraintViolationWithTemplate(String messageTemplate)
    사용자 정의 메시지를 추가
  • addConstraintViolation()
    새롭게 정의된 메시지를 유효성 검사 결과에 추가
  • getDefaultConstraintMessageTemplate()
    기본 메시지 템플릿을 반환
@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() 메서드가 검증하게 될 것이다.

ICustomValidator.java

public interface ICustomValidator {
    boolean validate(Object obj, String message) throws Exception;
}

 

UserCreateValidator.java

@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 메서드를 호출

 

UserUpdateValidator.java

@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;
    }
}

 

검증객체 (UserDto.java)

@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 어노테이션은 선언해 놨기 때문이다.

 

Controller 계층

@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 계층

@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가 호출된 것을 확인할 수 있다.

반응형

관련글 더보기

댓글 영역

>