프로젝트를 진행하며 컨트롤러에 들어오는 값에 대해 검증을 하는 로직을 작성해야 하는 상황이 발생했습니다...
원래대로라면 @Valid와 Bean Validation으로 슥슥 검증을 하였겠지만 컨트롤러로 들어오는 요청에는 Enum이 존재하였습니다.
그리고 Enum에 대한 검증은 물론 Enum은 자동으로 매핑되지도 않았습니다....
저는 고민을 하다 다음과 같이 로직을 작성하기로 마음먹었습니다.
- 컨트롤러로 들어오는 Enum을 문자열로 매핑시켜 받습니다.
- 이때 이 문자열에 대해 사용자 정의 어노테이션을 정의해 검증을 합니다.
- 만약 검증에 성공한다면 DTO안에 있는 값을 Enum으로 매핑시켜 로직을 수행합니다.
이제 이 포스팅의 주제이기도 한 사용자 정의 어노테이션을 만들어 보도록 하겠습니다.
먼저 필요한 것은 두가지 입니다.
하나는 사용자 정의 annotation 나머지 하나는 해당 어노테이션이 수행하는 로직을 담당하는 Validator입니다.
먼저 사용자 정의 annotation입니다.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CategoryValidator.class)
public @interface CategoryCheck {
String message() default "올바른 카테고리가 아닙니다.";
Class[] groups() default {};
Class[] payload() default {};
Class<? extends java.lang.Enum<?>> enumClass();
boolean ignoreCase() default false;
}
하나씩 뜯어서 설명을 하면 다음과 같습니다.
- @Target : 해당 어노테이션이 어디에 붙어서? 동작할지를 정하는 코드입니다. @Controller가 메소드 혹은 파라미터가 있을 위치에 존재하지 않고 클래스에 붙어있음을 생각해보시면 될 듯 합니다.
- @Retension : 해당 어노테이션이 언제까지 적용되어야 하는지 알려줍니다. 저는 서버가 계속해서 동작하고 있을때, Runtime일때 어노테이션이 정상적으로 동작하기를 바라기 때문에 Runtime 옵션을 넣어주었습니다.
- @Constraint : 이 어노테이션이 가리키는 로직은 어디에 있는지 알려줍니다. 저는 CategoryValidator에 제가 원하는 로직을 작성해놓았기에 해당 로직을 작성해놓았습니다.
- String message() default : 로직 수행에 실패했을때 무슨 문구를 출력할지를 정해줍니다. @NotNull("널이면 안됩니다!!") 와 같은 것과 같습니다.
- Class[] groups() default {}: 요 친구는 유효성을 검사할 Bean의 그룹들을 지정해줍니다.
- Class[] payload() default {} : 유효성 검사 결과를 저장할 수 있는 페이로드 유형을 지정합니다.
- Class<? extends java.lang.Enum<?>> enumClass(): 카테고리 값이 포함된 Enum 클래스를 지정합니다.
- boolean ignoreCase() default false: 대소문자를 구분할지 안할지 설정합니다.
사실 저기서 저희가 신경써야 할 것은 어떠한 메시지를 출력할 것인지, 어떠한 Validator를 설정할지를 정하는 코드 이외에는 크게 변하는 것이 없습니다.
다음은 실질적인 검증 로직이 존재하는 validator입니다.
public class CategoryValidator implements ConstraintValidator<CategoryCheck, String> {
private CategoryCheck annotation;
@Override
public void initialize(CategoryCheck constraintAnnotation) {
this.annotation = constraintAnnotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Object[] enumValues = this.annotation.enumClass().getEnumConstants();
if (enumValues != null && Arrays.stream(enumValues)
.anyMatch(enumValue ->
value.equals(enumValue.toString()) ||
(this.annotation.ignoreCase() && value.equalsIgnoreCase(enumValue.toString())))) {
return true;
}
return false;
}
}
저는 여기서 initialize 메서드를 통해서 어노테이션들이 무슨 값을 가지고 있는지 검증하기 위해 초기화해주어 저장해주었습니다.
이제 거의다 왔습니다!
사용자 정의 어노테이션의 핵심이라고도 할 수 있는 검증로직이 담긴 메서드를 오버라이드 해야 합니다.
이를 위해서 ConstraintValidator를 구현해야 합니다
저는 문자열이 enum에 존재하는지 확인하고자 하였기에 스트림을 통해 값이 있는지 확인하고 있다면 True 없다면 False를 반환해주면 됩니다
그러면..
public record ReportRequestDto(
@NotNull(message = "값을 입력해주세요.")
@PositiveOrZero(message = "0이상의 값을 입력해주세요.")
double latitude,
@NotNull(message = "값을 입력해주세요.")
@PositiveOrZero(message = "0이상의 값을 입력해주세요.")
double longitude,
@NotNull(message = "null이 아닌 값을 입력해주세요.")
String description,
@CategoryCheck(enumClass = Category.class)
String category,
@NotNull(message = "값을 입력해주세요.")
@PositiveOrZero(message = "0이상의 값을 입력해주세요.")
int damageRatio) {
}
다음과 같이 어노테이션을 사용할 수 있습니다!
'소소한 꿀팁..?' 카테고리의 다른 글
nGrinder 테스트 스크립트 ide에서 작성하기(+ intelliJ, docs) (0) | 2024.04.09 |
---|---|
GRASP 패턴 (0) | 2024.02.20 |
[Data JPA] delete구문에서의 Batch (2) | 2024.01.04 |
코드리뷰시 다른 사용자의 Comment 숨기기 (0) | 2023.10.27 |
깃 커밋 컨벤션 템플릿으로 설정하기 (0) | 2023.10.27 |