커스텀 어노테이션을 이용 validation

Choco·2024년 2월 20일
post-thumbnail

유효성 검사를 할떄 기존에 있는 어노테이션만으로 안될 경우가 있다.
예를 들어 특정한 값만을 요청하는 Enum 변수는 따로 어노테이션을 만들어서 해야 한다. 우선 Enum 유효성 검사를 위해 @ValidEnum 어노테이션을 만들어 보자.

@Constraint(validatedBy = EnumValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidEnum {
    String message() default "잘못된 변수 입니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    Class<? extends java.lang.Enum<?>> enumClass();
}

어노테이션

@Constraint(validatedBy = EnumValidator.class)

ConstraintValidator을 이용하여 validation을 하기 위해 지정하는 어노테이션이다. validatedBy를 이용하여 구현체를 지정할 수 있다.

@Target({ElementType.FIELD})

어노테이션을 쓸 곳을 정한다. 해당 어노테이션은 ReqDto에서 필드 값에다가 지정하기 때문에 ElementType.FIELD만을 지정한다.

@Retention(RetentionPolicy.RUNTIME)

어노테이션에 유효시간을 지정한다. 스프링이 실행될떄도 동작해야 하기에 validation은 스프링이 동작할때도 유효해야 하기에RetentionPolicy.RUNTIME으로 지정한다.

필드값

어노테이션의 필드값은 변수로 지정받을 수 있다. 변수가 없으면 default 값을 설정할 수도 있다.

예를 들어 @Requestmapping(path="/api")도 path 필드값이 선언되어 있는 것을 확인할 수 있다

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Reflective(ControllerMappingReflectiveProcessor.class)
public @interface RequestMapping {
	...
	String[] path() default {};
    ...

String message() default "잘못된 변수 입니다.";

오류 발생시 응답해줄 메세지 변수다. 지정한 메세지 값이 없으면 defalut 값으로 반환 된다.

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

validation을 그룹화 할때 설정 하는 값이다.

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

메타 데이터를 지정할 수 있다.

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

어떤 Enum 클래스에 대해서 validation을 할지 설정한다.

어노테이션 설정을 했으니 ConstraintValidator을 이용하여 EnumValidator 구현체를 작성해야 한다.

구현체

public class EnumValidator implements ConstraintValidator<ValidEnum,Enum> {
    private ValidEnum annotation;
    @Override
    public void initialize(ValidEnum constraintAnnotation) {
        this.annotation = constraintAnnotation;
    }

	//value를 통해 요청값을 받고 
    @Override
    public boolean isValid(Enum value, ConstraintValidatorContext context) {
		
		//커스텀 메세지를 작성하기 위해 기본 메세지를 disable 한다
        context.disableDefaultConstraintViolation();

        boolean result = false;
		// 값을 통해 지정한 enumClass에 있는 값들과 비교하는 코드를 작성한다.
        Object[] enumValues = this.annotation.enumClass().getEnumConstants();
        if (enumValues != null) {
            for (Object enumValue : enumValues) {
                if (value == enumValue) {
                    result = true;
                    break;
                }
            }
        }
		//지정된 값이 없을시 enumclass와 메세지를 보낸다.
        if(!result){
            context.buildConstraintViolationWithTemplate(this.annotation.enumClass() +this.annotation.message()).addConstraintViolation();
        }
        return result;
    }
}

Dto 및 Enum

Controller에서 RequestBody를 통해 ReqDatasetDto를 받고 Organization값만 받아야 하는 상황이다.

public enum Organization {
    공과대학,경상대학,디자인대학,약학대학,예체능대학,입학처;
}
@Data
public class ReqDatasetDto {
    @ValidEnum(enumClass = Organization.class, message = "잘못된 조직명 입니다")
    @Schema(description = "조직", example = "입학처")
    private Organization organization;
    
}

예외처리

valiation은 문제가 생기면 MethodArgumentNotValidException 에러가 발생한다. BindingResult를 이용하여 메시지를 반환한다.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<?>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
     ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
     String message = objectError.getDefaultMessage();
     log.warn(e.getMessage());
     return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.fail(message));
 }

하지만 역직렬화 할떄 정해진 Enum에 해당 값이 아니면 역직렬화 자체를 실패하므로, 유효성 검사전에 에러를 발생시킨다.

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `com.hanyang.dataportal.dataset.domain.Organization` from String "테스트": not one of the values accepted for Enum class:

그렇기에 역직렬화에 성공할 수 있게 하기위해 @JsonCreator를 이용하여 정해진 Enum값에 해당하지 않으면 null로 반환시키는 코드를 작성한다.

public enum Organization {
    공과대학,경상대학,디자인대학,약학대학,예체능대학,입학처;
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static Organization findByOrganization(String organization) {
        return Stream.of(Organization.values())
                .filter(o -> o.toString().equals(organization))
                .findFirst()
                .orElse(null);
    }
}
profile
주니어 백엔드 개발자 입니다:)

0개의 댓글