[Spring] Enum Type으로 Request 인자를 전달받을 때 Validation 처리하기

lsjbh45·2022년 9월 6일
3

Spring의 controller 단에서 특정 request의 body를 전달받을 때 사용하는 dto class에는 내부 field에 enum을 사용할 수 있다. 이렇게 만들어진 dto class에 application/json 형태의 body를 가진 request를 송신할 때, Java type이 enum인 field에 들어갈 payload 값이 해당 enum에 포함되지 않는 값이라면 JSON parse error가 발생함을 확인할 수 있다. 여기에서 Bean Validation을 사용하는 경우에 의문이 드는 부분이 존재하는데, 아무리 annotation을 명시한다고 해도 validator가 작동하기 전에 먼저 JSON parsing 과정에서 오류가 발생하고, validator가 무시되어 원하는 방식대로의 handling이 불가능하다는 점이다.

사실 기본적으로 발생하는 JSON parse error를 그대로 둔다고 해도 큰 문제가 발생하는 것은 아니다. 하지만 다른 field들과 유사하게 validation 상태를 표기하고, invalid한 data에 대해서는 동일한 형식으로 response를 보내도록 handling하기 위해서 Bean Validation 방식이 적용될 수 있도록 변경해 주는 것이 통일성 측면에서 의미가 있을 것이다. 이 글에서는 상황에 따라 Java enum type의 field를 포함하는 dto에 JSON 형태의 Request Body가 들어왔을 때 Bean Validation 처리를 하는 방식에 관해 논의해 볼 것이다.

인자를 Enum Type으로 전달받는 경우

@JsonCreator(mode=JsonCreator.Mode.DELEGATING)
public static MyType get(String code) {
	return Arrays.stream(values())
		.filter(type -> type.getCode().equals(code))
		.findAny()
		.orElse(null);
}

인자를 그대로 enum type의 field에 받아오고자 한다면, request body의 JSON 형태 data가 parsing되는 과정에서 enum type의 field에 들어갈 수 있도록 payload 값이 parsing될 때 error가 발생하지 않도록 해야 한다. JsonCreator annotation을 사용한다면 JSON 형태의 value들을 Java type으로 변환하는 방식을 직접 method로 지정해 줄 수 있다. Bean Validation을 지정하려는 enum class들을 정의할 때 method를 추가해 주고 annotation을 명시해 주는 방식으로 간단하게 conversion을 지정하게 된다.

특히 기본적으로 JSON parsing 시에는 enum의 특정 field만을 accepting하는 logic을 정의할 수 없지만, 직접 method를 지정하는 경우 특정 field에 해당하는 값에 대해서만 변환이 이루어지도록 명확히 표현해 예상치 못한 문제가 발생하지 않도록 할 수 있다. 만약 일치하는 값이 없다면 null을 return하도록 method를 정의해서 parsing error가 발생하지 않도록 정의해 주어야 한다.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy=EnumValidValidator.class)
public interface EnumValid {
	String message() default "Invalid Enum";

	Class<?> groups() default {};

	Class<? extends Payload>[] payload() default {};
}
public class EnumValidValidator implements ConstraintValidator<EnumValid, Enum<?>> {
	@Override
	public void initialize(EnumValid constraint) {

	}

	@Override
	public boolean isValid(Enum value, ConstraintValidatorContext constraintValidatorContext) {
		return value != null;
	}
}

enum class를 정의할 때 JsonCreator annotation을 명시해 parsing method를 지정해 주었다면
conversion이 잘 이루어졌을 경우애는 해당 enum type의 value를, 잘못된 값이 들어왔다면 null 값을 변환 후에 가지고 있게 될 것이다. JSON 변환 후에 validation이 이루어지기 때문에, enum type에 대한 validation을 위해서는 어떤 enum type인지와 관계 없이 null 값 여부만 확인해 주면 된다. 이 점을 확인하기 위해 dto의 enum type field에 사용할 EnumValid annotation과 EnumValidValidator class를 정의해 줄 수 있다.

public static class MyDto {
	@EnumValid
	private MyType val;
}

인자를 일반 Type으로 전달받는 경우

계층별로 dto를 따로 정의해 계층 이동에 따라 변환시켜 사용하도록 설계가 되어있다면, controller 단에서 JSON data를 enum type으로 받아오는 것이 아니라 하위 계층으로의 dto 변환 과정에서 enum type으로 변환해 주도록 dto를 정의해 줄 수도 있다. 이 경우에는 request body의 data가 parsing되는 과정에서 String, Integer 등 일반적인 type으로의 변환만 발생하므로 JSON parse error가 발생할 일이 없고, Bean Validation으로 데이터의 accept 여부를 확인해 주면 된다.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy=EnumValidValidator.class)
public interface EnumValid {
	String message() default "";

	Class<?> groups() default {};

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

	Class<? extends CodeEnum> target();
}
public class EnumValidValidator implements ConstraintValidator<EnumValid, Object> {
	List<Object> validList;

	@Override
	public void initialize(EnumValid constraint) {
		validList = Arrays
			.stream(constraint.target().getEnumConstants())
			.map(CodeEnum::getCode)
			.collect(Collectors.toList());
	}

	@Override
	public boolean isValid(Enum value, ConstraintValidatorContext constraintValidatorContext) {
		boolean isValid = validList.contains(value);

		if(!isValid) {
			constraintValidatorContext
				.buildConstraintViolationWithTemplate(
					format("{Invalid Input for Code Field of Enum: Should be in %s}", validList.toString())
				)
				.addConstraintViolation()
				.disableDefaultConstraintViolation();
		}
		
		return isValid;
	}
}

이상은 이전 글에서 정의해 보았던 CodeEnum interface에 대해서 정의한 validator와 annotation이다. request body의 value가 controller 단에서는 변환될 enum의 code field의 값과 compatible하다는 가정에 따라, request body의 value가 enum의 code field가 가질 수 있는 값들에 포함되어 있는지를 검증하게 된다. annotation class인 EnumValid에는 기본적인 message, groups, payload 이외에 target field를 정의해 어떤 enum class에 대해 검증할지를 지정할 수 있다. target field로는 CodeEnum interface를 구현한 class만을 지정할 수 있도록 해서 code field를 기준으로 검증이 이루어지게 된다.

EnumValidValidator class의 정의에 따르면 null 값에 대한 확인은 자동으로 진행되기 때문에, 만약 validator에서 enum class의 code field 값들 뿐만 아니라 null도 허용하도록 하기 위해서는 정의를 다소 변경해 이를 명시해 주어야 한다. ConstraintValidatorContext를 사용해서는 validation 과정의 데이터를 활용한 error message를 지정할 수 있는데, 여기에서는 accept되는 값들을 나타내도록 formatting이 이루어져 있다.

enum ValType implements CodeEnum {
	/* ... */
}

public static class MyDto {
	@EnumValid(ValType.class)
	private String val;
}
profile
개발을 공부하며 깊게 고민했던 트러블슈팅 과정을 공유하고자 합니다.

1개의 댓글

comment-user-thumbnail
2023년 4월 5일

Request Dto에서 Enum 타입을 받고, validation하는 부분에 있어서 이해에 큰 도움 되었습니다!!

답글 달기