[Spring-Boot] Enum 유효성 검사하기

도비·2023년 10월 3일
0

Spring Boot

목록 보기
2/13
post-thumbnail

Enum 유효성 검사하기

요약
@Patternenum 은 같이 쓸 수 없어서 커스텀 어노테이션을 만들어 @JsonCreator 와 같이 사용해서 처리했다.

Request Body의 유효성 검증 중 Enum에 대한 유효성 검증은 로컬에서 따로 정해주지 않아도 400 Bad Request로 Response가 넘어와서 그대로 배포를 진행했는데 빈 스트링을 넘겨줬을 때 500 에러가 넘겨졌다.

에러 로그를 살펴보니 json parse error 발생하였다.

보통 @Valid 어노테이션과 Validation이 제공하는 어노테이션을 사용하면 유효하지 않은 요청이 올 경우 MethodArgumentNotValidException을 발생시킨다. 그런데 Enum의 경우 해당 요청에 대한 Validation을 따로 제공하고 있지 않다.

Validation 할 객체

public enum Duration {
    HALF("HALF"),
    HOUR("HOUR"),
    HOUR_HALF("HOUR_HALF"),
    TWO_HOUR("TWO_HOUR"),
    TWO_HOUR_HALF("TWO_HOUR_HALF"),
    THREE_HOUR("THREE_HOUR");
}
@AllArgsConstructor
public enum Place {
    ONLINE("ONLINE"),
    OFFLINE("OFFLINE"),
    UNDEFINED("UNDEFINED");
}
public enum TimeSlot {
    SLOT_6_00("06:00"),
    SLOT_6_30("06:30"),
    SLOT_7_00("07:00"),
    SLOT_7_30("07:30"),
    SLOT_8_00("08:00"),
    SLOT_8_30("08:30"),
    SLOT_9_00("09:00"),
    SLOT_9_30("09:30"),
    SLOT_10_00("10:00"),
    SLOT_10_30("10:30"),
    SLOT_11_00("11:00"),
    SLOT_11_30("11:30"),
    SLOT_12_00("12:00"),
    SLOT_12_30("12:30"),
    SLOT_13_00("13:00"),
    SLOT_13_30("13:30"),
    SLOT_14_00("14:00"),
    SLOT_14_30("14:30"),
    SLOT_15_00("15:00"),
    SLOT_15_30("15:30"),
    SLOT_16_00("16:00"),
    SLOT_16_30("16:30"),
    SLOT_17_00("17:00"),
    SLOT_17_30("17:30"),
    SLOT_18_00("18:00"),
    SLOT_18_30("18:30"),
    SLOT_19_00("19:00"),
    SLOT_19_30("19:30"),
    SLOT_20_00("20:00"),
    SLOT_20_30("20:30"),
    SLOT_21_00("21:00"),
    SLOT_21_30("21:30"),
    SLOT_22_00("22:00"),
    SLOT_22_30("22:30"),
    SLOT_23_00("23:00"),
    SLOT_23_30("23:30"),
    SLOT_24_00("24:00");
}

이렇게 Request Body에 사용해야하는 enum이 여러 개였는데, 클라이언트 측에서 enum value에 해당하지 않는 값을 넘겨주었을 경우 500 response가 넘어왔다.

enum을 검증하려면 커스텀 어노테이션을 만들어야 한다.

Enum 검증하는 커스텀 어노테이션 만들기

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Constraint(validatedBy = EnumPatternValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumPattern {
    String regexp();

    String message() default "does not match \"{regexp}\"";
    
		Class<?>[] groups() default {};
    
		Class<? extends Payload>[] payload() default {};
		
		Class<? extends java.lang.Enum<?>> enumClass();
}
  • message : 오류 발생 시 생성할 메세지
    • 정의한 @EnumPattern을 사용해서 오류가 났을 때 메시지 출력
  • groups(): 상황별 validation 제어를 위해 사용
  • payloads(): 심각도 나타냄. 거의 사용하지 않음
  • enumClass(): 제약할 클래스를 지정
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EnumPatternValidator implements ConstraintValidator<EnumPattern, Enum<?>> {
    private Pattern pattern;

    @Override
    public void initialize(EnumPattern annotation) {
        try {
            pattern = Pattern.compile(annotation.regexp());
        } catch (PatternSyntaxException e) {
            throw new IllegalArgumentException("pattern regex is invalid", e);
        }
    }

    @Override
    public void isValid(Enum<?> value, ConstraintValidatorContext context) {
        if (value == null) {
	          throw new BadRequestException(Error.INVALID_VALUE_EXCEPTION);   
        }
    }
}

isValid() 제약 조건을 설정해서 value가 null이면 BadRequestException을 던지게 설정한다.

Validation에 대한 설정은 끝났다.

@JsonCreator를 enum 안 메서드에 적어주면 된다.

@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static TimeSlot findByTime(String timeSlot) {
        return Stream.of(TimeSlot.values())
            .filter(c -> c.code.equals(timeSlot))
            .findFirst()
            .orElseThrow(() -> BadRequestException(Error.INVALID_VALUE_EXCEPTION));
    }
}

그 후 Request Body에 Enum 값 위에 만든 @EnumPattern 을 통해 받아야 하는 형식을 지정해준다.

@EnumPattern(regexp = "\\d\\d:\\d\\d", message = "입력된 시간 형식이 유효하지 않습니다.", enumClass = TimeSlot.class)
private TimeSlot startTime;

그 후 만약 잘못 요청일 들어오면 MethodArgumentNotValidException이 발생한다.

이제 MethodArgumentNotValidException 에러를 Adviser에서 잡아준다.

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ErrorResponse handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) {
    return ErrorResponse.error(Error.VALIDATION_REQUEST_MISSING_EXCEPTION);
}

잘못된 요청을 보내면 다음과 같이 유효성 검사를 실패했다는 메세지로 400 response를 넘겨준다.

Enum Validation 하는 과정은 끝입니다, 긴 글 읽어주셔서 감사합니다.

profile
하루에 한 걸음씩

1개의 댓글

comment-user-thumbnail
2024년 8월 11일

이걸 이제보다니.. ENUM만 BindingResult로 못잡아줘서 문제였는데 감사합니당~!

답글 달기