
유효성 검사를 할떄 기존에 있는 어노테이션만으로 안될 경우가 있다.
예를 들어 특정한 값만을 요청하는 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();
}
ConstraintValidator을 이용하여 validation을 하기 위해 지정하는 어노테이션이다. validatedBy를 이용하여 구현체를 지정할 수 있다.
어노테이션을 쓸 곳을 정한다. 해당 어노테이션은 ReqDto에서 필드 값에다가 지정하기 때문에 ElementType.FIELD만을 지정한다.
어노테이션에 유효시간을 지정한다. 스프링이 실행될떄도 동작해야 하기에 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 {};
...
오류 발생시 응답해줄 메세지 변수다. 지정한 메세지 값이 없으면 defalut 값으로 반환 된다.
validation을 그룹화 할때 설정 하는 값이다.
메타 데이터를 지정할 수 있다.
어떤 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;
}
}
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);
}
}