Java Annotation 정리

 스프링을 사용해서 개발을 하다보면 @RestController @Service @Component 같은 Annotation을 자주 접하게 된다. 스프링에서는 이러한 Annotation들을 컴포넌트 스캔 과정에서 메타 정보로 활용하고, 빈을 생성하여 IoC 컨테이너를 구성한다. 이처럼 어노테이션은 코드의 메타 데이터를 제공하는 역할을 한다.

 어노테이션을 활용하면 컴파일 타임, 실행 시점에 해당 메타 정보를 제공하여 동적으로 특정 코드를 생성하거나 기능을 수행시킬 수 있다.

본 글에서는 어노테이션을 구성하는 각 요소들과 커스텀 어노테이션 생성 방법을 정리해보고자 한다.

Annotation

 어노테이션을 정의 할때는 Java에 내장된 몇 가지 메타 어노테이션들을 같이 활용한다.
자주 사용되는 메타 어노테이션은 다음과 같다.

  • @Retention : 컴파일 ~ 런타임 과정 중 어느 단계까지 유지되어야 하는지를 결정한다.
  • @Target : 어노테이션 선언이 가능한 위치 정보를 설정한다.
  • @Documented : Javadoc에 관련 정보를 함께 생성할 것인지 여부를 결정한다.
  • @Inherited : 상위 타입에 어노테이션이 적용되어 있는 경우, 하위 타입에 이를 전파할 것인지 결정한다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ExampleAnnotation {

    String value() default "This is example.";

    boolean visible() default true;
}


@Retention

 Java는 소스 코드를 작성하고, 작성된 소스 코드를 컴파일하고, 컴파일 된 바이트 코드가 클래스 로더에 의해 JVM에 올라가는 과정을 거쳐서 실행된다.

Alt Image

여기서 @Retention 은 Annotation이 어느 단계까지 유지되어야 하는지를 결정한다. SOURCE, CLASS, RUNTIME 세가지 타입이 있다.

Alt Image

RetentionPolicy.SOURCE

 컴파일 단계에서 컴파일러에게 관련 정보를 제공하지만, 바이트 코드(.class)에는 기록되지 않는다.

 익숙한 @Override로 예를 들어보면, 서브 타입에서 메서드 오버라이딩을 하는 경우, @Override 가 선언되어 있다면 컴파일 타임에 메서드 시그니처가 잘 선언되어 있는지 검증한다.
그러나 바이트 코드에는 @Override가 남아있지 않다.


RetentionPolicy.CLASS

 컴파일 단계를 거쳐 바이트 코드까지 유지되지만 런타임에는 유지되지 않는다.


RetentionPolicy.RUNTIME

 런타임까지 유지되며, 리플렉션을 통해 해당 메타 정보에 접근할 수 있다.


Alt Image



CLASS VS RUNTIME

CLASSRUNTIME 의 중요한 차이점은 리플렉션 활용 가능 여부이다. RUNTIME으로 선언되어야만 실행 중에도 해당 메타 정보를 계속 참조할 수 있다.

코드를 통해 확인해보자.

RetentionPolicy.CLASS

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ClassRetentionAnnotation {

}
@ClassRetentionAnnotation
public class ClassRetentionTarget {

}
class ReflectionTest {

    @Test
    void doClassRetentionTest() {
        ClassRetentionAnnotation annotation = ClassRetentionTarget.class.getAnnotation(ClassRetentionAnnotation.class);

		// RetentionPolicy.CLASS 인 경우 리플렉션으로 활용 불가
        then(annotation).isNull();
    }

Alt Image



RetentionPolicy.RUNTIME

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeRetentionAnnotation {

}
@RuntimeRetentionAnnotation
public class RuntimeRetentionTarget {

}
class ReflectionTest {

    @Test
    void doRuntimeRetentionTest() {
        RuntimeRetentionAnnotation annotation = RuntimeRetentionTarget.class.getAnnotation(RuntimeRetentionAnnotation.class);

        then(annotation).isNotNull();
    }
}

Alt Image



@Target

 어노테이션이 어디에 위치할 수 있는지를 결정한다.

 예를 들어 클래스에 지정하려면 ElementType.TYPE이 선언되어 있어야 하고, 메서드에 적용하기 위해서는 ElementType.METHOD 가 필요하다.

 타겟은 여러개 지정 가능하며, 다음과 같이 선언한다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface MultipleTargetAnnotation {

	// ...
}
ElementType Description
TYPE 클래스, 인터페이스, 어노테이션 타입, 열거 타입에 적용
FIELD 필드(멤버 변수)에 적용
METHOD 메서드에 적용
PARAMETER 메서드 파라미터에 적용
CONSTRUCTOR 생성자에 적용
LOCAL_VARIABLE 지역 변수에 적용
ANNOTATION_TYPE 어노테이션에 적용
PACKAGE 패키지 선언에 적용
TYPE_PARAMETER 제네릭 타입 선언의 타입 파라미터 (클래스, 메서드)
(Java 8 이상)
TYPE_USE 필드, 메서드 리턴 타입, 메서드 파라미터, 생성자 파라미터 등 타입 사용의 모든 경우에 적용 가능
(Java 8 이상)
MODULE 모듈 선언 파일 (module-info.java)에 적용
(Java 9 이상)
RECORD_COMPONENT 레코드의 각 컴포넌트(레코드 내에 선언된 필드)에 적용 가능 (Java 16 이상)


Element

 어노테이션 내부에 요소들을 정의하고, 어노테이션이 사용되는 선언부에 특정 값을 할당하여 사용할 수 있다. 해당 메타 정보들은 런타임에 리플렉션을 통해 접근 할 수 있다.

요소는 다음과 같이 반환 타입, 참조 될 이름, 기본 값 으로 구성된다.

// ...
public @interface ExampleAnnotation {

	/**
	 * Element 1
	 */
    String value() default "This is example.";

	/**
	 * Element 2
	 */
	boolean visible() default true;
}

 첫 번째 요소인 value 에는 기본 값으로 This is example. 이 설정되어있고, visible 에는 true가 지정되어 있다.

 만약 다음과 같이 value에 다른 값을 할당하면 value 에는 해당 값이 저장된다. 반면 visible에는 별도 값을 지정하지 않았기 때문에 기본 값으로 설정되어 있는 true가 유지된다.

@ExampleAnnotation(value = "This is value.")
public class ExampleAnnotationTarget {

}
class AnnotationElementTest {

    @Test
    void doTest() {
        ExampleAnnotation annotation = ExampleAnnotationTarget.class.getAnnotation(ExampleAnnotation.class);

        // value에 지정한 값을 참조한다.
        then(annotation.value()).isEqualTo("This is value.");

        // visible에 default로 선언된 true 값을 가진다.
        then(annotation.visible()).isTrue();
    }
}


@Documented

@Documented는 Javadoc 생성 과정에서의 메타 정보를 제공한다. 만약 @Documented가 선언되어 있는 경우, Javadoc에 해당 어노테이션 정보를 함께 기록한다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Documented   // <-
@Retention(RetentionPolicy.RUNTIME)
public @interface ExampleAnnotation {

    String value() default "This is example.";

    boolean visible() default true;
}
/**
 *  `@ExampleAnnotation`에 `@Documented` 가 선언되어 있는 경우
 *   `ExampleAnnotationTarget`의 Javadoc에 
 *   어노테이션 정보가 추가된다.
 */
@ExampleAnnotation(value = "This is value.")
public class ExampleAnnotationTarget {

}

Alt Image

만약 @Documented 가 없는 경우, 아래와 같이 @ExampleAnnotation 정보를 찾을 수 없다.

Alt Image



@Inherited

@Inherited 는 서브 타입으로의 어노테이션 전파 여부를 결정한다.
예를 들어 상위 클래스에 특정 어노테이션이 선언되어 있다고 해보자.
 만약 어노테이션에 @Inherited 가 선언되어 있다면 하위 클래스에 직접 선언하지 않더라도 해당 어노테이션이 동일하게 적용된다.

@Target(ElementType.TYPE)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface ParentAnnotation {

}
@ParentAnnotation
public class ParentClass {

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChildAnnotation {

}
/**
 *  `@ParentAnnotation`이 선언되어 있지 않다.
 */
@ChildAnnotation
public class ChildClass extends ParentClass {

}
class InheritedAnnotationTest {

    @Test
    void doTest() {
        ParentAnnotation parentAnnotation = ChildClass.class.getAnnotation(ParentAnnotation.class);
        ChildAnnotation childAnnotation = ChildClass.class.getAnnotation(ChildAnnotation.class);

		/**
		 *  ChilClass에 `@ParentAnnotation`이 없음에도
		 *  직접 참조할 수 있다.
		 */
        then(parentAnnotation).isNotNull();
        then(childAnnotation).isNotNull();
    }
}

Alt Image

만약 @ParentAnnotation에 선언되어 있는 @Inherited 를 제거하면, 아래와 같이 테스트가 실패하게 된다.

Alt Image



Custom Annotation 생성 및 적용 예시

 지금까지 어노테이션 관련 기본 내용들을 살펴보았다. 지금부터는 커스텀한 어노테이션을 직접 재정의 해보면서 해당 내용들을 다시 정리해보고자 한다. 해당 예시에서는 기존에 익숙하게 사용했을 법한 @Transactional 어노테이션을 활용하였다.

 JPA를 사용할 때 데이터 읽기 작업만을 수행하는 경우, 불필요한 더티 체킹이 발생하지 않도록 @Transactional(readOnly=true) 를 지정한다.

이를 커스텀한 어노테이션으로 재정의하고 사용해보자.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(readOnly = true)
public @interface ReadOnlyTransactional {

}
  • @Target : 기존 @Transactional 과 동일하게 클래스, 메서드에 모두 사용할 수 있도록 ElementType.TYPE, ElementType.METHOD 를 속성으로 지정하였다.
  • @Retention : 런타임에 참조될 수 있도록 하기 위해 RetentionPolicy.RUNTIME 을 설정하였다.

@Service
public class TransactionService {

	/**
	 * `@Transactional(readOnly=true)`과 동일한 동작을 한다.
	 */
    @ReadOnlyTransactional
    public boolean isReadOnlyTransaction() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly();
    }
}
@SpringBootTest
class TransactionServiceTest {

    @Autowired
    private TransactionService transactionService;

    @Test
    void readOnlyTransactionTest() {
        boolean readOnlyTransaction = transactionService.isReadOnlyTransaction();

        then(readOnlyTransaction).isTrue();
    }
}

Alt Image



References