
API 개발을 하면서 유효성 검사를 진행하다 보면, 기본 어노테이션으로는 검증이 불가능한 경우가 있다. 그럴 때 직접 어노테이션을 개발해서 유효성 검사를 진행할 수 있다. 이렇게 유효성 검사를 위한 커스텀 어노테이션을 만들어두면 다른 곳에서도 편리하게 사용할 수 있다.
빈 관리, 의존성 주입, 트랜잭션 관리 등 다양한 기능을 처리할 수 있다. 특히 Spring Boot에서는 어노테이션을 통해 설정을 최소화하고 자동화를 구현할 수 있다. 다만, 어노테이션을 사용하면 로직이 숨겨지기 때문에 무분별하게 사용하는 것은 좋지 않다.
이번에는 어노테이션을 통해 DTO 유효성 검사를 진행하고자 한다.
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Annotation {
...
}
어노테이션을 정의할 때는 다음과 같은 메타 어노테이션을 사용한다.
@Target : 어노테이션의 적용 대상을 지정한다.@Retention : 어노테이션의 지속 시간을 지정한다.@Inherited : 어노테이션이 상속되도록 설정한다.@interface로 어노테이션 클래스를 정의한다. @interface로 정의하면 컴파일러가 자동으로 java.lang.Annotation을 상속하도록 처리해준다.default 키워드로 기본값을 설정할 수 있다. 다만, null로는 설정할 수 없다.Class.getAnnotation(), Method.getAnnotation() 등을 통해 어노테이션 정보를 얻을 수 있다.| ElementType | 적용 대상 |
|---|---|
TYPE | Class, Interface |
FIELD | 객체 필드 (enum, 상수 포함) |
METHOD | 메소드 |
PARAMETER | 매개변수 |
CONSTRUCTOR | 생성자 |
LOCAL_VARIABLE | 지역 변수 |
ANNOTATION_TYPE | 어노테이션 |
PACKAGE | 패키지 |
TYPE_PARAMETER | 매개변수의 타입 |
TYPE_USE | 매개변수 사용 시 |
@Target에서 어노테이션 적용 대상을 지정하는 옵션이다.
유효성 검사 시에는 주로 FIELD, METHOD, PARAMETER, TYPE_USE를 많이 사용한다.
| RetentionPolicy | 유지 시점 |
|---|---|
SOURCE | 컴파일러 사용 후 삭제 |
CLASS | 클래스 파일에는 포함, 런타임에는 접근 불가 |
RUNTIME | 런타임까지 유지 (프로그램에서 접근 가능) |
@Retention에서 어노테이션 유지 시점을 지정하는 옵션이다.
커스텀 어노테이션을 만들 때는 어플리케이션 동작에 영향을 주는 RUNTIME을 가장 많이 사용한다.
@Documented
@Constraint(validatedBy = LevelFormatValidator.class)
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE })
@Retention(RetentionPolicy.RUNTIME)
public @interface LevelFormat {
String message() default "점수는 0.5 단위로 입력해 주세요.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
boolean nullable() default false;
}
@Documented@ContraintvalidatedBy로 실제 검증 로직을 구현할 클래스를 지정한다.message() : 검증 실패 시 반환할 메시지.groups() : 검증 그룹을 지정한다. (선택적 검증용)payload() : 검증 메타데이터를 전달한다. (심각도, 카테고리 등)nullable() : 해당 필드의 null 가능 여부를 지정한다.public class LevelFormatValidator implements ConstraintValidator<LevelFormat, Float> {
private Boolean nullable;
public void initialize(LevelFormat levelFormat) {
this.nullable = levelFormat.nullable();
}
@Override
public boolean isValid(Float value, ConstraintValidatorContext context) {
// nullable이 true이고 value가 null일 경우 바로 검증을 성공 처리한다
if (value == null) {
return nullable; // value가 null이고 nullable이 false라면 검증을 실패 처리한다
}
return (value * 2) % 1 == 0; // 0.5 단위인지 확인 (예: 2.5 * 2 = 5, 3.4 * 2 = 6.8)
}
}
ConstraintValidator 인터페이스를 상속받아서 실제 검증 로직을 구현한다. 검증할 어노테이션 타입과 검증 대상 데이터의 타입을 정의하면, Bean Validation이 이 인터페이스를 통해 검증 로직을 호출한다.
initialize() : 어노테이션 속성값을 받아서 Validator를 초기화한다isValid() : 실제 검증 로직을 구현한다@LevelFormat(nullable = false)
@Schema(description = "만족도", example = "3.5")
private Float level;
만든 어노테이션을 실제 필드에 적용하면 된다. @Valid 혹은 @Validated를 함께 사용해야 자동으로 필드 유효성 검사를 진행한다.
이렇게 간단하게 커스텀 어노테이션을 만들어서 DTO 유효성 검사를 진행할 수 있다. 다른 유효성 검사를 진행하고 싶다면 또다른 어노테이션을 만들어서 진행하면 된다. 만든 어노테이션은 다른 곳에서도 자유롭게 사용할 수 있다.
public class LevelFormatValidator implements ConstraintValidator<LevelFormat, Float> {
@Override
public boolean isValid(Float value, ConstraintValidatorContext context) {
if (value == null) return nullable;
if ((value * 2) % 1 != 0) {
// 기본 메시지 비활성화
context.disableDefaultConstraintViolation();
// 상황에 맞는 커스텀 메시지 생성
String customMessage = String.format("입력값 %.1f는 0.5 단위가 아닙니다. 예: 1.0, 1.5, 2.0", value);
context.buildConstraintViolationWithTemplate(customMessage)
.addConstraintViolation();
return false;
}
return true;
}
}
입력값에 따라 동적으로 메세지를 생성하고 싶은 경우에는 ConstraintValidatorContext를 사용할 수 있다.
disableDefaultConstraintViolation() : 어노테이션의 기본 메세지를 비활성화한다buildConstraintViolationWithTemplate() : 동적으로 생성한 메시지를 설정한다addConstraintViolation() : 새로운 제약 조건을 추가한다[Spring Boot] Annotation 개념 이해하기, 주요 어노테이션
[Java] @Retention, @Target에 대하여
Spring Annotation의 원리와 Custom Annotation 만들어보기
커스텀 어노테이션(Custom Annotation) 만들기