🚀 프로젝트/🥁 두둥

[Spring] 스프링에서 Enum 클래스 Validation 구현하기 (Enum JSON parse error 해결)

gengminy 2023. 2. 21. 13:37

스프링에서 일반적으로 RequestBody 의 값을 Validation 하는 방법

스프링 MVC 에서 @Valid 어노테이션을 명시해주면

컨트롤러에서 해당 값에 대해 미리 Validation 해줘서 값이 들어오게 된다.

 

JAVAX 에서 @NotBlank, @NotNull, @Positive 등등

원시 타입에 대해서는 여러 가지 기본 검증을 지원한다.

 

하지만 Enum 클래스에 대한 Validation 은 기본으로 지원하지 않는다.

그러면 어떤 식으로 처리해야 할까?

 

 

📌 Enum Validation 처리를 하지 않았을 경우

Enum 필드에 대해 Validation 처리하지 않는다면

Enum 클래스에 속하지 않은 값이 Request 필드에 포함되면

JSON 값을 Enum 으로 올바르게 파싱하지 못하여

위 처럼 JSON parse error 가 응답으로 오게 된다.

 

하지만 백엔드의 입장에서도 그렇고 500 에러와 더불어 이런 에러는 상당히 보기가 싫다

그러니까 이러한 예외 처리를 더불어

에러 메세지까지 보내주는게 정신건강에 좋을 거 같지 않은가?

 

 

📌 Enum Validation 적용 과정

📝 EventStatus

@Getter
@AllArgsConstructor
public enum EventStatus {
    // 준비중
    PREPARING("PREPARING", "준비중"),
    // 진행중
    OPEN("OPEN", "진행중"),
    // 정산중
    CALCULATING("CALCULATING", "정산중"),
    // 지난 공연
    CLOSED("CLOSED", "지난공연"),
    // 삭제된 공연
    DELETED("DELETED", "삭제된공연");

    private final String name;
    @JsonValue private final String value;

    // Enum Validation 을 위한 코드, enum 에 속하지 않으면 null 리턴
    @JsonCreator
    public static EventStatus fromEventStatus(String val) {
        return Arrays.stream(values())
                .filter(type -> type.getName().equals(val))
                .findAny()
                .orElse(null);
    }
}

Validation 을 사용하고 싶은 Enum Class 에

다음과 같이 @JsonCreator 어노테이션을 명시한 정적 메소드를 추가하자

@JsonCreator 는 페이로드에서 JSON -> Enum 필드로 파싱하는 방법을 직접 명시해주게 된다.

 

fromEventStatus 메소드에서는 Enum Class 에 속하지 않는 값이 들어올 때 null 을 리턴하게 되서

JSON parsing error 가 발생하지 않도록 해줬다.

 

이렇게만 해도 JSON parsing error 는 응답으로 오지 않지만

잘못된 Enum 값에 대한 예외 처리도 해주고 싶다.

 

 

📝 Enum

@Constraint(validatedBy = {EnumValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Enum {
    String message() default "Invalid Enum Value.";

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

    Class<? extends Payload>[] payload() default {};
    
    Class<? extends java.lang.Enum<?>> target();
}

커스텀 어노테이션 인터페이스를 작성해준다

  • @Constraint 는 Validation 하려는 메소드가 정의된 클래스
  • @Target 은 이 어노테이션이 붙을 수 있는 범위
  • @Retention 은 이 어노테이션의 생명주기를 지정해준다

또한 Custom Constraint Annotation 을 지정할 때는 다음 3개를 꼭 정의해야 한다 

  • message 는 예외 발생 시 응답 메세지 지정
  • group 은 Validation 그룹 지정
  • payload 는 추가 정보를 위해 지정할 수 있으며 주로 심각도를 나타내는 필드

마지막으로 target 은 이 Validator 가 적용해줄 범위를 지정한다

Class<? extends Enum> 은 java.lang.Enum 클래스를 상속받는 모든 인자라는 뜻을 제네릭으로 표현한 것이다

즉, 이 인자에는 Enum 클래스만 올 수 있다는 뜻이 된다

 

target 필드는 Request 에 무조건 Enum 타입만 넣는다고 가정하면 없어도 되지만,
다른 타입의 값을 Request 로 받아 Enum 으로 넣어주고 싶은 상황이 생길 수도 있으니
범용성을 위해 추가하였다.

 

📝 EnumValidator

public class EnumValidator implements ConstraintValidator<Enum, java.lang.Enum> {
    private Enum annotation;

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

    @Override
    public boolean isValid(java.lang.Enum value, ConstraintValidatorContext context) {
        Object[] enumValues = this.annotation.target().getEnumConstants();
        if (enumValues != null) {
            for (Object enumValue : enumValues) {
                if (value.equals(enumValue.toString())) {
                    return true;
                }
            }
        }
        return false;
    }
}

EnumValidator 에서는 Enum 의 value 값들을 가져오기 위해 멤버 변수로 지정하고

initailize 메소드를 오버라이딩 하여 이 값을 할당한다.

 

이후 isValid 메소드를 오버라이딩 하여 검증 로직을 실행한다.

target 클래스에서 Enum 값들을 가져와 검증하려는 value 와 비교하게 된다.

 

무조건 Enum 값이 들어온다고 가정하고 작성하면 아래와 같이 줄일 수 있다.

public class EnumValidator implements ConstraintValidator<Enum, java.lang.Enum> {
    @Override
    public boolean isValid(java.lang.Enum value, ConstraintValidatorContext context) {
        return value != null;
    }
}

@JsonCreator 에서 해당 Enum 클래스에 없는 값이 들어오면

NULL 로 파싱해주기 때문에 이 로직을 적용할 수 있다

 

 

📝 UpdateEventStatusRequest

@Getter
@RequiredArgsConstructor
public class UpdateEventStatusRequest {
    @Schema(defaultValue = "OPEN", description = "오픈 상태")
    @Enum(target = EventStatus.class, message = "올바른 값을 입력해주세요.")
    private EventStatus status;
}

이제 Request 에서 원하는 Enum 필드에 적용해보자.

target 에는 내가 검증하고자 하는 Enum 클래스를 지정하면 된다.

 

 

처음에는 이런 식으로 예외처리가 안되어서 에러 메세지가 그대로 노출이 될텐데

@RestControllerAdvice 에서 MethodArgumentNotValidException 핸들링을 해줘

적절하게 에러 메세지를 가공하면

 

이런 식으로 각 필드별로 메세지를 깔끔하게 정리할 수 있다

 

 

 

🌎 구현 과정 관련 게시글

📎 Custom Enum Validator 구현하기

 https://gengminy.tistory.com/47

📎 Reflection 을 이용하여 Enum Validator 개선하기

https://gengminy.tistory.com/48

📎 Custom Enum Deserializer 구현하여 Enum 에 없는 값 null 로 파싱하기

https://gengminy.tistory.com/49