[Spring] DTO에서 UUID, Enum 검증하기

sinryuji·2025년 2월 21일
post-thumbnail

현재 진행중인 프로젝트에서 Entity id를 UUID로 설정해서 DTO에서 UUID를 검증 할 필요가 있어졌다. 그래서 @Valid를 통해 UUID를 검증하는 방법과 추가로 DTO에서 Enum을 받는 케이스도 많으니 그 경우까지 알아보자.

Valid UUID

여타 validation 어노테이션들과 비슷하게 사용하기 위해 어노테이션 형식으로 구현해보자. 일단 커스텀 어노테이션을 하나 만들어야 한다.

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = {UUIDValidator.class})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUUID {

    String message() default "유효하지 않은 UUID 입니다!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
  • @Target: 어노테이션을 붙일 수 있는 곳을 지정한다. 필드와 파라미터에 붙일 수 있도록 해주었다.
  • @Constraint: 원하는 제약 조건을 설정할 수 있다. 나는 따로 ConstraintValidator 인터페이스의 구현체를 만들어 지정해 줄 것이다.
  • @Retention: 어노테이션의 라이프사이클을 지정해준다. 런타임 까지 살아있어야 하기에 RetentionPolicy.RUNTIME로 설정해준다.

💡 RetentionPolicy의 종류

  • RetentionPolicy.SOURCE : 소스 코드(.java)까지 남아있는다.
  • RetentionPolicy.CLASS : 클래스 파일(.class)까지 남아있는다.
  • RetentionPolicy.RUNTIME : 런타임까지 남아있는다.
public class UUIDValidator implements ConstraintValidator<ValidUUID, UUID> {

    @Override
    public boolean isValid(UUID value, ConstraintValidatorContext context) {
        // UUID가 null이면 유효하지 않음
        if (value == null) {
            return false;
        }

        // UUID의 문자열 표현이 유효한지 검증
        try {
            UUID.fromString(value.toString());
            return true; // 유효한 UUID 형식
        } catch (IllegalArgumentException e) {
            return false; // 유효하지 않은 UUID 형식
        }
    }
}

실제 검증에 사용될 ConstraintValidator의 구현체이다. ConstraintValidator<A extends Annotation, T> 인터페이스를 구현한다. A는 사용될 어노테이션, T는 검층할 데이터의 자료형이다.

isValid() 함수를 구현해야 하며 boolean을 리턴하여 검증 성공 여부를 전달한다.

UUID 값이 null이거나, fromString()에서 exception이 발생하면 false를 리턴해준다.

    public static UUID fromString(String name) {
        if (name.length() == 36) {
            char ch1 = name.charAt(8);
            char ch2 = name.charAt(13);
            char ch3 = name.charAt(18);
            char ch4 = name.charAt(23);
            if (ch1 == '-' && ch2 == '-' && ch3 == '-' && ch4 == '-') {
                long msb1 = parse4Nibbles(name, 0);
                long msb2 = parse4Nibbles(name, 4);
                long msb3 = parse4Nibbles(name, 9);
                long msb4 = parse4Nibbles(name, 14);
                long lsb1 = parse4Nibbles(name, 19);
                long lsb2 = parse4Nibbles(name, 24);
                long lsb3 = parse4Nibbles(name, 28);
                long lsb4 = parse4Nibbles(name, 32);
                if ((msb1 | msb2 | msb3 | msb4 | lsb1 | lsb2 | lsb3 | lsb4) >= 0) {
                    return new UUID(
                            msb1 << 48 | msb2 << 32 | msb3 << 16 | msb4,
                            lsb1 << 48 | lsb2 << 32 | lsb3 << 16 | lsb4);
                }
            }
        }
        return fromString1(name);
    }

java.util.UUIDfromString() 함수이다. UUID 형식까지 야무지게 검사해준다.

    @ValidUUID
    private UUID storeId;

이제 앞서 구현한 커스텀 어노테이션을 DTO의 UUID 필드에 붙여주면 사용할 수 있다.

Valid Enum

마찬가지로 커스텀 어노테이션을 하나 만들어준다.

@Constraint(validatedBy = EnumValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidEnum {

    Class<? extends Enum<?>> enumClass();

    String message() default "유효하지 않은 Enum 값입니다.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Class<? extends Enum<?>> enumClass()를 통해 검증할 Enum 클래스를 지정해준다. default 값이 없으면 필수로 전달받아야 한다.

그 후 검증을 수행할 ConstraintValidator 구현체를 하나 만들어준다.

public class EnumValidator implements ConstraintValidator<ValidEnum, Enum<?>> {

    private Enum<?>[] enumValues;

    @Override
    public void initialize(ValidEnum constraintAnnotation) {
        enumValues = constraintAnnotation.enumClass().getEnumConstants();
    }

    @Override
    public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
        if (value == null) {
            return false; // null 값 허용 X
        }
        return Arrays.asList(enumValues).contains(value);
    }
}

어느 Enum 클래스든 상관 없이 사용하려면 T에 Enum<?>을 주면 된다.

initialize() 함수를 통해 초기화 시 로직을 작성할 수 있다. default 함수이므로 구현하지 않아도 된다. 앞서 ValidEnum에서 전달받은 enumClass에서 enum value들을 enumValues에 담아주었다. 그 후 isValid()에서 value가 그 enum value 중 존재하는지를 확인하는 로직이다.

    @ValidEnum(enumClass = DeliveryType.class)
    private DeliveryType deliveryType;

역시나 위와 같이 enum class 필드에 붙여 사용 할 수 있다. 앞서 설명했다시피 검증할 enum class의 정보가 필요하므로 반드시 전달해주어야 한다.

profile
응애 개발자입니다.

0개의 댓글