JAVA/Spring Boot

Annotation) 커스텀 어노테이션으로 Enum 유효성 검사하기

주코식딩 2023. 3. 13. 21:18

Request에서 String을 받아 Enum으로 변환하는 작업을 해야한다.

받아온 String이 Enum에 속한 값인지 판단하는 Validation Annotation을 생성하고자 한다.

물론 애초에 convertor를 통해 Request에서 직접 Enum타입을 받는 방법도 있다.

다만, 후자의 경우에는 convertor도 구현하고 Enum Validator도 구현해야하기에 비용이 적게드는 첫 번 째 방법을 선택했다.

 

어노테이션을 만든다는 것은 생각 보다 어렵지 않았다. 무엇보다  Enum Validator는 이미 코드가 많기 때문에 구현에 어려움은 없었는데

문제는 List로 된  Enum은 구글링으로 찾기 어려웠기에 직접 구현했다.

Valid에 사용되는 어노테이션은 예시가 많기 때문에 구현에 어려움은 없었다.

 

일단 EnumValidator부터 살펴보자.

 

EnumValidator

어노테이션을 다양한 클래스에서 사용할 수 있도록 를 많이 활용한 모습이다.

/* https://funofprograming.wordpress.com/2016/09/29/java-enum-validator/ */
/* https://velog.io/@hellozin/Annotation%EC%9C%BC%EB%A1%9C-Enum-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0 */

/* 해당 annotation이 실행 할 ConstraintValidator 구현체를 `EnumValidatorImpl`로 지정합니다. */
@Constraint(validatedBy = {EnumValidatorImpl.class})
/* 해당 annotation은 메소드, 필드, 파라미터에 적용 할 수 있습니다. */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
/* annotation을 Runtime까지 유지합니다. */
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumValidator {
    String message() default "Invalid value. This is not permitted.";

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

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

    Class<? extends java.lang.Enum<?>> enumClass();

    boolean ignoreCase() default false;
}

 

EnumValidatorImpl

ConstraintValidtor를 Override해서 isValid 함수를 구현하고 그 안에 로직을 작성하면 쉽게 쉽게 customValid 어노테이션을 만들 수 있다.

initialize 함수에는 제네릭에 사용한 Enumvalidator가 입력변수로 오게 되는데 이를 클래스 변수에 넣어서 위에 작성했던 변수에 담긴 값(실제 사용시 어노테이션에 주었던 옵션 값)을 가져올 수 있다.

 

추가로 나는 Enum에 description이라는 변수를 함께 담는걸 좋아해서 해당 부분을 interface로 만든뒤 사용할 Enum에 implements 해서 유효성검사를 할 수 있게 작성했다.

/**
 * {@link ConstraintValidator}에서 구현된 메소드를 찾아보면 예시를 알 수 있다.
 */
public class EnumValidatorImpl implements ConstraintValidator<EnumValidator, String> {

    private EnumValidator annotation;

    @Override
    public void initialize(EnumValidator constraintAnnotation) {
        this.annotation = constraintAnnotation;
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        boolean result = false;
        Object[] enumValues = this.annotation.enumClass().getEnumConstants();
        if (enumValues != null) {
            for (Object enumVal : enumValues) {
                if (enumVal instanceof DescriptionEnum enumValue) {
                    if (value.equals(enumValue.getDescription())
                            || (this.annotation.ignoreCase() && value.equalsIgnoreCase(enumValue.getDescription()))) {
                        result = true;
                        break;
                    }
                } else {
                    if (value.equals(enumVal.toString())
                            || (this.annotation.ignoreCase() && value.equalsIgnoreCase(enumVal.toString()))) {
                        result = true;
                        break;
                    }
                }
            }
        }
        return result;
    }
}

 

ListEnumValidator

이름으로 봐서 알겠지만 나는 List안에 담긴 Enum도 유효성 검사를 하고 싶었다. 여러 옵션 값이나 권한을 가진 Entity 컬럼을 위해 필요하다.

@Constraint(validatedBy = {ListEnumValidatorImpl.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListEnumValidator {
    String message() default "Invalid value. This is not permitted.";

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

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

    Class<? extends java.lang.Enum<?>> enumClass();

    boolean ignoreCase() default false;
}

 

ListEnumValidatorImpl


public class ListEnumValidatorImpl implements ConstraintValidator<ListEnumValidator, List<String>> {
    private ListEnumValidator annotation;

    @Override
    public void initialize(ListEnumValidator constraintAnnotation) {
        this.annotation = constraintAnnotation;
    }

    @Override
    public boolean isValid(List<String> value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) {
            return true;
        }

        for (String val : value) {
            boolean isValid = false;

            for (Enum<?> enumVal : this.annotation.enumClass().getEnumConstants()) {
                if (enumVal instanceof DescriptionEnum enumValue) {
                    String enumValDescription = enumValue.getDescription();
                    String enumValName = enumVal.name();
                    if (annotation.ignoreCase()) {
                        if (enumValDescription.equalsIgnoreCase(val) || enumValName.equalsIgnoreCase(val)) {
                            isValid = true;
                            break;
                        }
                    } else {
                        if (enumValDescription.equals(val) || enumValName.equals(val)) {
                            isValid = true;
                            break;
                        }
                    }
                } else {
                    String enumValDescription = enumVal.name();
                    if (annotation.ignoreCase()) {
                        if (enumValDescription.equalsIgnoreCase(val)) {
                            isValid = true;
                            break;
                        }
                    } else {
                        if (enumValDescription.equals(val)) {
                            isValid = true;
                            break;
                        }
                    }
                }
            }

            if (!isValid) {
                return false;
            }
        }

        return true;
    }
}

 

 

글을 쓰다보니 description으로만 유효성검사을 할지 name으로만 유효성 검사를 할지 정하는 옵션을 추가해도 좋을 것 같다.

물론 둘다 한번에 유효성검사를 하게끔 코드를 짤 수 는 있지만 결국 Enum객체로 변환하는 과정에서는 하나의 방법만 정해두는것이 편하기 때문에 올바른 방법은 아니라고 생각한다.

 

마지막으로 Enum과 만들어둔 Interface를 공유한다.


@Getter
@NoArgsConstructor
public enum UserRole implements DescriptionEnum {
    ADMIN("관리자"),
    USER("유저"),
    SYSTEM_ADMIN("시스템 관리자");

    private String description;

    UserRole(String description) {
        this.description = description;
    }

    public static UserRole of(String description) {
        for (UserRole userRole : UserRole.values()) {
            if (userRole.getDescription().equalsIgnoreCase(description)) {
                return userRole;
            }
        }
        return null;
    }

}
public interface DescriptionEnum {
    String getDescription();
}