Spring

Enum Validation

Tommy__Kim 2023. 4. 24. 17:07

Backend Service를 구축 시 Controller 단에서 요청을 받을 때
해당 요청이 유효한 요청인지 검증하기 위해 Dto와 @Valid를 함께 자주 사용합니다.

@Valid 어노테이션과, Validation이 제공하는 어노테이션을 사용하면
유효하지 않은 요청사항이 올 경우 미리 MethodArgumentNotValidException을 발생시켜 줍니다.

자세한 설명에 앞서 간단한 예시를 확인하고 넘어가도록 하겠습니다.

가정 사항은 Controller단에서 Dto로 요청을 받아 Service에서 회원을 가입시키는 상황입니다.

// RequestDto
@Getter
public class RequestDto {
    @NotNull(message = "null 일 수 없습니다.")
    @NotBlank(message = "공백 입력은 불가합니다.")
    private String email;

    private String gender;
}

// Controller
@RestController
@RequiredArgsConstructor
public class TestController {
    private final TestService testService;

    @GetMapping("/test")
    public String test(@RequestBody @Valid RequestDto requestDto) {
        Member member = testService.save(requestDto);
        return member.getEmail();
    }
}


// Service
@Service
public class TestService {

    public Member save(RequestDto requestDto) {
        Gender gender;
        String requestGender = requestDto.getGender();
        if (requestGender.equals("male")) {
            gender = Gender.MALE;
        } else if (requestGender.equals("female")) {
            gender = Gender.FEMALE;
        } else {
            gender = Gender.FEMALE;
        } 
        return new Member(requestDto.getEmail(), gender);
    }
}

// Member
@Getter
public class Member {
    private String email;
    private Gender gender;

    public Member(String email, Gender gender) {
        this.email = email;
        this.gender = gender;
    }
}

// Gender
public enum Gender {
    MALE, FEMALE
}

Java의 Primitive Type 혹은 String, Integer 등의 경우 적용할 수 있는 Validation Annotation이 많습니다.
하지만 Enum의 경우 해당 요청에 대한 기본 Validation을 따로 제공하고 있지는 않습니다.

만약 해당 요청사항에 "male", "female"이 아닌 잘못된 값이 온다면 Member의 Gender는 FEMALE이 됩니다.

Enum Class에 대해서도 @Valid를 적용해 잘못된 요청을 막으면 좋겠지만 기본으로 제공되는 Validation은 없습니다.

따라서 이번 장에서는 Enum Class에 대한 @Valid 적용을 할 수 있는 방법을 알아보려고 합니다.

다른 값들이 오는 경우에 대해 Exception을 발생시켜도 되지만 
해당 예제에서는 Enum Validation이 주된 관점이므로 이를 생략하도록 하겠습니다. 

@NotNull의 구성 

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {

	String message() default "{jakarta.validation.constraints.NotNull.message}";

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

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

	/**
	 * Defines several {@link NotNull} annotations on the same element.
	 *
	 * @see jakarta.validation.constraints.NotNull
	 */
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	@interface List {

		NotNull[] value();
	}
}
  • @Target : 해당 어노테이션을 적용할 범위를 지정합니다. 
  • @Retention : Runtime시 유지되며, Runtime 동안 프로그램에서 접근이 가능합니다. 
  • @Constraint : 어떠한 Class에 의해 유효성 판단을 할지 지정합니다.

이와 같이 비슷하게 구현을 하면 될 것 같습니다. 

 

Enum Validator 구현 

Enum Class를 비교하기 위해 EnumValue 라는 어노테이션을 생성해 주겠습니다. (원하는 클래스명 만드시면 됩니다.)

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = ValueOfEnumValidator.class)
public @interface EnumValue {
    Class<? extends Enum<?>> enumClass();

    String message() default "";

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

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

    boolean ignoreCase() default false;
}
  • message : 오류 발생 시 생성할 메세지 입니다. 
  • groups() : 상황별 validation 제어를 위해 사용됩니다.
  • payloads() : 심각도를 나타냅니다. 
  • ignoreCase() : 대소문자를 구별할 것인지 정하는 boolean 값입니다. 

@NotNull과 유사하지만 ValueOfEnumValidator에 의해 검증이 되는 방식입니다.

 

public class ValueOfEnumValidator implements ConstraintValidator<EnumValue, String> {

    private EnumValue enumValue;
    @Override
    public void initialize(EnumValue constraintAnnotation) {
        this.enumValue = constraintAnnotation;
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        boolean result = false;
        Enum<?>[] enumValues = this.enumValue.enumClass().getEnumConstants();
        if (enumValues != null) {
            for (Object enumValue : enumValues) {
                if (value.equals(enumValue.toString())
                        || this.enumValue.ignoreCase() && value.equalsIgnoreCase(enumValue.toString())) {
                    result = true;
                    break;
                }
            }
        }
        return result;
    }
}

실질적인 유효성을 검증하는 method는 isValid 입니다.

this.enumValue.enumClass().getEnumConstants()를 통해 Enum 클래스에 있는 값들을 불러옵니다. 

클래스에 있는 값들을 비교하면서 해당 값이 존재하는지 검증을 합니다. 

this.enumValue.ignoreCase() && value.equalsIgnoreCase(enumValue.toString())

앞서 설명했던 EnumValue에 ignoreCase가 있을 경우 대소문자 구분을 무시하고 값을 비교하도록 구현하였습니다. 

 

@Getter
public class RequestDto {
    @NotNull(message = "null 일 수 없습니다.")
    @NotBlank(message = "공백 입력은 불가합니다.")
    private String email;
    @EnumValue(enumClass = Gender.class, message = "유효하지 않은 성별입니다.", ignoreCase = true)
    private String gender;
}

이제 RequestDto에 gender에 @EnumValue를 입력해 줍니다. 

추가적으로 오류가 생겼을 때 나타낼 message를 적어주고, ignoreCase 여부를 결정해 줍니다. 

Exception Handler 구현 

@ControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity handleException(MethodArgumentNotValidException e) {
        return ResponseEntity.badRequest().body(e.getFieldError().getDefaultMessage());
    }
}

Validation 검증이 유효하지 않다면 MethodArgumentNotValidException 예외가 발생합니다. 

이러한 예외를 잡아 요청에 대한 응답으로 보낼 수 있게끔 구현하였습니다. 

 

[정상 요청 - 대문자]

[정상 요청 - 소문자]

[유효하지 않은 성별]

해당 예제는 다음 링크에서 확인해 보실 수 있습니다. Github 바로가기


출처 : https://funofprograming.wordpress.com/2016/09/29/java-enum-validator/

'Spring' 카테고리의 다른 글

[스프링] 빈 생명주기 콜백에 관하여  (0) 2023.05.25
Spring의 핵심 Concept는 무엇인가  (0) 2023.05.23
Spring Bean의 생명주기  (0) 2023.04.21
싱글톤에 대해서  (2) 2023.04.10
Dependency Injection (의존 관계 주입)  (0) 2023.04.10