🤔 들어가기 앞서
사실 enum에 대한 유효성 검사를 위해서 Custom Annotation
을 구현할 필요는 없을 수 있다.
하지만 나는 DTO에서 enum 타입 자체를 가지고 있고, 구현한 enum 타입이 많아서 만들었다.
이를 통해서, Parse 오류도 잡고 클라이언트에서 어떤 값을 잘못 보냈는지 확인할 수 있게 되었다! 😋😋
Custom Annotation
을 구현하는 방법은 이전에 작성한 글을 참고하기 바란다!
🪄 Enum용 CustomAnnotation 만들기
작성한 코드는 우선 다음과 같다. 핵심으로 표시한 부분은 다음에 설명하겠다.
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumFormatValidator.class)
public @interface EnumFormat {
String message() default "해당 필드의 타입에서 지원하지 않는 값입니다.";
Class[] groups() default {};
Class<? extends Enum> enumClass(); // --- 핵심
Class[] payload() default {};
}
🛠️ 유효성 검사기 구현하기
작성한 코드를 보면 다음과 같다. 직관성을 위해서 메서드를 최대한 분리하는 편이다 ㅎㅎ
public class EnumFormatValidator implements ConstraintValidator<EnumFormat, Enum> {
private List<Object> enumValues;
@Override
public void initialize(EnumFormat constraintAnnotation) {
enumValues = getEnumValues(constraintAnnotation);
}
@Override
public boolean isValid(Enum value, ConstraintValidatorContext context) {
return isNotNull(value) && isContainsValue(value);
}
private List<Object> getEnumValues(EnumFormat constraintAnnotation) {
Class<? extends Enum> enumClass = constraintAnnotation.enumClass();
return Arrays.stream(enumClass.getEnumConstants()).collect(Collectors.toList());
}
private boolean isContainsValue(Enum value) {
return enumValues.contains(value);
}
private boolean isNotNull(Enum value) {
return value != null;
}
}
가장 먼저 초기화를 진행한다. Reflection API
를 통해서 검사하는 enum의 값들을 가져온다.
@NotNull
을 사용하지 않고 자체적으로 null
에 대한 검사를 진행하고 싶어서 검사 메서드를 추가하였고,
포함 여부는 enum에 수백, 수천 가지의 값을 넣을 것 같진 않아서 contains()
로 검사하였다.
처음에 고민했던 점은, 이 유효성 검사에 어떻게 다양한 enum에 모두 공통적으로 적용시킬 것인가였다.
Reflection API
로 enum의 값들은 쉽게 가져올 수 있었지만 정작 enum이 어떤 녀석인지 알 수가 없었다.
따라서, 조금 원시적인 방법이지만 직접 어떤 enum을 검사할 것인지 넘겨주는 방식을 선택했다.
public class MyDto {
@EnumFormat(enumClass = MyEnum.Class) // Enum의 클래스를 명시
private MyEnum myEnum;
}
그 결과, 유효성 검사를 진행할 때 constraintAnnotation.enumClass()
로 enum의 클래스를 가져올 수 있게 되었다.
모든 enum에 대한 유효성 검사를 할 수 있게 되었단 뜻이다. 😋😋
💣 새로운 문제 직면
"생각보다 너무 쉽네"라는 생각과 "당연히 잘되겠지"라는 생각으로 테스트를 진행해 봤다.
엥? 나를 반겨주는 것은 400 Error
였다. 이게 무슨..
😨 원인 분석
우선, 내 의도는 클라이언트에서 null, 빈 값, 그리고 enum에 없는 값을 보내면 검사 실패를 반환할 생각이었다.
발생하는 에러는 Json parse Error
로, 클라이언트에서 HTTP Body
에 담아 보낸 Json 객체를 스프링에서 일치하는 Java 객체로 변환할 때 발생하는 에러였다.
따라서, 이 에러는 유효성 검사가 발생하기 이전에 발생해서 원하는 대로 예외를 핸들링할 수도 없다.
🥲 문제 해결하기
구글링과 ChatGPT의 도움으로 사방팔방으로 찾아봤지만 내가 원하는 해결법은 하나도 없었다.
찾은 해결법 중 하나는 @JsonCreator
사용하기였다.
@JsonCreator
는 DTO 객체로 매핑할 때 사용할 생성자를 지정해 주는 방법이다.
분명히 내가 직면한 문제를 해결할 수 있지만 enum 타입이 매우 많다면 그건 또 다른 문제였다.
즉, 모든 enum 타입에 생성자를 작성해줘야 하는데 이건 개발자답지 않다고 생각했다.
그래서 근원적으로 접근하기로 생각했다.
다시 Json parse Error
가 발생 시점으로 돌아가면, Json 객체가 Java 객체로 변환될 때 문제이다.
변환되는 과정을 역직렬화
라고 하는데, 역직렬화될 때 매핑되는 값을 수정하면 될 것 같았다.
유효성 검사기에서 null
인 경우 바로 false
를 리턴하도록 만들었으니,
만약 DTO의 enum 필드에 들어온 값이 빈 값 혹은 다른 값이라면 null을 매핑하도록 만들면 될 것 같았다.
🔐 CustomEnumDeserializer 구현하기
구현한 CustomEnumDeserializer
는 다음과 같다.
public class CustomEnumDeserializer<T extends Enum<T>> extends JsonDeserializer<T> {
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
final String value = p.getValueAsString();
final Class<?> dtoClass = p.getCurrentValue().getClass();
final String fieldName = p.currentName();
try {
final Class<T> enumClass = getEnumClass(dtoClass, fieldName);
return Enum.valueOf(enumClass, value);
} catch (IllegalArgumentException | NoSuchFieldException e) {
return null;
}
}
private static <T extends Enum<T>> Class<T> getEnumClass(Class<?> dtoClass, String fieldName) throws NoSuchFieldException {
Field field = dtoClass.getDeclaredField(fieldName);
return (Class<T>) field.getType();
}
}
각 메서드의 리턴 값은 다음과 같다.
getValueAsString()
: 클라이언트에서 넘어온 필드의 값을String
으로 변환getCurrentValue().getClass()
: 역직렬화하는 DTO의 클래스currentName()
: 역직렬화하는 DTO의 필드의 이름 (ex. private MyEnum myEnum의 myEnum)
여기서 핵심은 Reflection API
와 Enum.valueOf()
이다.
이 Custom 역직렬화 메서드는 모든 enum에 대해서 적용되야 하기 때문에 enum의 클래스를 가져와야 한다.
그렇다면, "위에서 작성한 것처럼 사용하면 되지 않을까?"라고 생각할 수 있지만 사용방법 자체가 다르다.
// CustomEnumDeserializer 적용법
@JsonDeserialize(using = CustomEnumDeserializer.class)
public enum MyEnum {
// ...
}
역직렬화 시 파라미터로 들어오는 값을 통해서는 확인해야 할 enum의 클래스를 알 수 있는 방법이 없었다.
그래서 Reflection API
를 사용했고, DTO의 클래스와 필드의 이름으로 enum의 클래스를 가져왔다.
따라서, 동적으로 확인하기 때문에 특정 타입을 명시하지 않아도, 수많은 코드를 작성할 수고도 덜었다.
그다음으론 어떤 enum인지 알았으니 클라이언트에서 넘어온 값으로 해당 타입으로 변환시킬 수 있는지 확인한다.
변환 메서드로 Enum.valueOf()
를 사용하는데 변환할 수 없을 때는 IllegalArgumentException
을 반환한다.
따라서, 이 예외가 발생하면 아까 말한 대로 null을 매핑시켜 주면되는 것이다.
😋 정리
enum 타입에 유효한 값이 들어왔는지 검사하는 어노테이션을 구현했다.
정해진 값이 들어오지 않았을 때 Json parse Error
가 발생했는데, 해결방법 중 하나인 @JsonCreator
는 너무 많은 코드를 작성해야 했다.
따라서, CustomEnumDeserializer
를 구현했고 이를 통해서 매핑되는 값을 제어해 줬다.
나 같은 고민을 가진 분이 있다면 이런 방법을 사용해 보시는 것도 좋을 것 같다!
사실, Reflection API
이렇게 막 사용해도 되는지는 잘 모르겠다.
이 내용에 대해서는 다음에 제대로 한번 다뤄봐야겠다.
😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋
'Web > BackEnd' 카테고리의 다른 글
[Backend] 트랜잭션, 격리 수준 (0) | 2023.07.27 |
---|---|
[BackEnd] MapStruct 사용기 (3) | 2023.05.13 |
[AWS] AWS 인프라 구축하기 - CodeDeploy (5) (0) | 2023.04.05 |
[AWS] AWS 인프라 구축하기 - SES (4) (0) | 2023.04.05 |
[AWS] AWS 인프라 구축하기 - S3 (3) (0) | 2023.04.04 |
댓글