Technology/Java

애노테이션을 정의하는 방법과 메타 애노테이션의 종류

ikjo 2022. 10. 11. 00:09

해당 글은 자바에서 애노테이션을 정의하는 방법과 기본적으로 제공하는 애노테이션들 중 메타 애노테이션에 대해 다루는 글로, 자바에서 사용되는 애노테이션에 대한 기초 개념과 표준 애노테이션에 대해선 아래 글을 참고할 수 있다.

 

 

Java의 애노테이션(Annotation) 기초

잠깐! 알쓸신잡, Annotation의 한글 표기법 : 애노테이션? 어노테이션? ※ TMI : Method → 메소드 X, 메서드 O 애노테이션이란? 애노테이션의 사전적인 뜻은 '주석'으로서 소스 코드의 동작에 직접적인

ikjo.tistory.com

 

애노테이션을 정의하는 방법

앞서 애노테이션 기초를 다룰 때에는 단순히 자바에서 기본적으로 제공하는 표준 애노테이션을 사용하는 방법에 대해서만 다루었는데, 이번에는 애노테이션을 직접 정의하는 방법에 대해 간단히 알아보고자 한다.

 

애노테이션은 인터페이스의 일종!

참고로 저번에 enum을 다루면서 enum은 클래스의 일종이라고 했는데, 애노테이션은 인터페이스의 일종이라고 할 수 있으며 정의하는 방법 또한 인터페이스를 정의하는 것과 유사하다.

 

다음 코드는 TestAnnotation이라는 애노테이션의 타입을 정의한 것인데, @ 표시는 애노테이션 타입을 나타내는 표식(Sign)을 나타낸다. 이러한 정의를 통해 TestAnnotation 애노테이션 즉, 우리에게 익숙한 형태인 @TestAnnotation으로 쓰일 수 있는 것이다.

 

@interface TestAnnotation {
    // ...
}

 

애노테이션의 요소 선언하기

인터페이스와 동일하게 이러한 애노테이션 내에는 '예외와 매개변수가 없는 추상 메서드'를 선언할 수 있는데, 이를 애노테이션의 요소(element)라고 한다. (이와 별개로 애노테이션 내에 상수도 정의할 수 있다.)

 

@interface TestAnnotation {

    String author();
    String date();
    int currentRevision() default 1;
    String lastModified();
    String[] reviewer();

}

 

요소를 정의한 애노테이션을 사용할 때에는 반드시 각 요소별로 이름과 지정할 값을 명시해주어야하는데, (순서는 상관없음) 기본값(default)을 가지는 요소의 경우엔 생략할 수 있다.

 

@TestAnnotation(
    author = "ikjo", 
    date = "2022-10-10", 
    lastModified = "2022-10-10", 
    reviewer = {"pio", "min"} // 값이 하나 일때에는 괄호를 생략할 수 있으며, 값이 없을 때는 {}로 명시한다.
)
public class Test {
    // ...
}

 

또한 요소가 하나뿐이고 요소의 이름이 value일 경우엔 요소의 이름을 생략하고 값만 명시할 수 있다. 아래 예시 코드를 참고해보자.

 

@TestAnnotation("ikjo")
public class Test {

}

@interface TestAnnotation {

    String value();

}

 

참고로 앞서 요소의 반환 타입을 String(배열 포함)과 int로 한정했지만, 이외에도 기본형(primitive type), enum, 애노테이션, Class 타입도 가능하다.

 

모든 애노테이션의 상위 타입은 Annotation 인터페이스!

모든 애노테이션 타입은 어떤 인터페이스나 애노테이션도 상속할 수 없다. 다만, 모든 애노테이션의 상위 타입은 java.lang.annotation 패키지의 Annotation 인터페이스(애노테이션 X)로 모든 애노테이션 객체에 대해 Annotaion 인터페이스에 정의된 메서드들을 호출할 수 있다. 참고로 Annotaion 인터페이스에 정의된 메서드는 다음과 같다.

 

package java.lang.annotation;

public interface Annotation {

    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
    
}

 

 

메타 애노테이션의 종류

지금까지 애노테이션 타입을 정의하는 방법에 대해 간단히 알아보았다. 사실 앞서 정의했었던 것은 단순히 요소를 선언하는데 그쳤는데, 이는 주석(comments)으로 코드에 대한 정보를 제공하는 것을 대체하는 것에 지나지 않는다. 이때, 자바에서 제공하는 메타 애노테이션을 이용하면 보다 부가적인 기능을 추가할 수 있게 된다.

 

앞서 언급했듯이 메타 애노테이션은 자바에서 기본적으로 제공하는 애노테이션으로서 새로운 애노테이션을 정의할 때 사용되는 애노테이션이다. 즉, 애노테이션에 붙이는 애노테이션이다. 그렇다면 메타 애노테이션에는 어떤 종류가 있을까?

 

@Target

@Target은 애노테이션이 적용 가능한 대상을 지정하는데 사용된다. Tartget 애노테이션 타입에는 아래와 같이 value라는 ElementType 배열 요소 하나가 정의되어 있다. 이때 ElementType은 '적용 대상별 상수(대상 타입)'를 지정해놓은 열거형이다.

 

java.lang.annotation 패키지 내 Target 애노테이션 타입

 

java.lang.annotation 패키지 내 ElementType(enum)

 

이때 적용 대상이 되는 종류는 다음과 같은데, @Target을 붙여줄 때 요소 값에 적용하고자 하는 대상 타입(ElementType)의 상수들을 명시해주면된다. (앞서 언급했듯이 요소가 1개이고, 요소 이름이 value일 때는 요소 이름을 생략하여 명시할 수 있다.)

 

 

※ TYPE, TYPE_PARAMETER, TYPE_USE은 Java 8부터 사용 가능하다.

 

아래 예시를 참고해보자.

 

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@TestAnnotation // TYPE
public class Test<@TestAnnotation T> { // TYPE_PARAMETER
    
}

@Target({ElementType.TYPE, ElementType.TYPE_PARAMETER})
// @Target({ElementType.TYPE_USE}) // 타입이 사용되는 모든 곳에 사용 가능 (TYPE, TYPE_PARAMETER 모두 포함)
@interface TestAnnotation {

}

 

 

또한 이전에 다루었던 표준 애노테이션 중 @SafeVarargs에는 선언된 @Target에는 적용 대상이 생성자와 메서드로 한정되어있다는 것을 확인할 수 있다. 제네릭이나 매개변수화 타입의 가변인자를 가지는 메서드 또는 생성자에 사용됨으로써 컴파일 경고 메시지를 억제하는 용도에 맞게 적용 대상이 지정된 것이다.

 

@SafeVarargs에 선언된 @Target

 

@Retention

@Retention은 애노테이션이 유지되는 기간(정책)을 정하는데 사용된다. Retention 애노테이션 타입에는 아래와 같이 value라는 RetentionPolicy 요소 하나가 정의되어 있으며, RetentionPolicy은 '애노테이션 유지 정책별 상수'를 지정해놓은 열거형이다.

 

java.lang.annotation 패키지 내 Retention 애노테이션 타입

 

java.lang.annotation 패키지 내 RetentionPolicy(enum)

 

위 RetentionPolicy 열거형을 보면 알 수 있듯이 유지 정책의 종류는 SOURCE, CLASS, RUNTIME 3개로 나뉜다.

 

우선 SOURCE 유지 정책의 경우, 애노테이션이 소스 코드 파일에서만 존재하고, 클래스 파일에는 존재하지 않는 유지 정책이다. 이를 사용하는 표준 애노테이션으로는 @Override와 @SuppressWarnings 등이 있다. 이들은 컴파일러가 특정 오류를 감지하도록 하거나 컴파일러가 경고 메시지를 표시하지 않도록 억제하는 등 컴파일러의 동작에 영향을 주지만, 컴파일 이후에는 사용되지 않으므로 컴파일러에 의해 버려진다.

 

SOURCE 유지 정책을 갖는 @Override

 

또한, CLASS 유지 정책의 경우, 컴파일러가 애노테이션 정보를 클래스 파일에 저장하긴 하지만, 실제 클래스 파일이 JVM에 로딩될 때에는 애노테이션 정보가 유지되지 않아 실행 시에는 사용될 수 없다.

 

마지막으로 RUNTIME 유지 정책의 경우, 컴파일러에 의해 애노테이션 정보가 클래스 파일에 저장되며 또한 클래스 파일이 JVM에 로딩된 이후로도 애노테이션 정보가 유지된다. 실행 중에는 리플렉션(reflection)을 통해 클래스 파일에 저장된 애노테이션의 정보를 읽어 처리할 수 있다.

 

@Documented

자바에서는 /** ~ */ 형태의 Doc Comments를 이용하여 javadoc 프로그램으로 HTML 문서를 생성할 수 있는데, 마찬가지로 @Documented를 이용하면 애노테이션에 대한 정보를 읽어 javadoc가 생성한 문서에 포함시킬 수 있다. 참고로 자바에서 기본적으로 제공하는 애노테이션 중에선 @Override와 @SuppressWarnings를 제외한 모든 애노테이션에 @Dcumented가 붙어있다.

 

@Documented가 적용된 @SafeVarargs

 

@Inherited

@Inherited는 애노테이션이 하위 클래스에 상속되도록 한다. 바로 예를 들어보자.

 

import java.lang.annotation.Inherited;

class Child extends Parent { // @TestAnnotation 를 붙인 것과 동일!!
    
}

@TestAnnotation
class Parent {

}

@Inherited
@interface TestAnnotation {
    
}

 

Parent 클래스에 @TestAnnotation을 붙였고 Child 클래스는 Parent 클래스를 상속했다. 비록 Child 클래스에는 @TestAnnotation이 붙지 않았지만 TestAnnotation 애노테이션 타입에 @Inherited가 붙었으므로 Child 클래스에도 @TestAnnotation이 붙은 효과가 생긴 것이다.

 

@Repeatable

일반적으로 한 대상에 한 종류의 애노테이션을 하나만 붙일 수 있는데, @Repeatable을 이용하면 한 대상에 한 종류의 애노테이션을 여러 번 붙일 수 있다. 마찬가지로 바로 예시 코드를 살펴보자.

 

import java.lang.annotation.Repeatable;

@TestAnnotation("ikjo")
@TestAnnotation("java")
class Test {

}

@Repeatable(TestAnnotations.class) // 괄호 안에 컨테이너 애노테이션 지정
@interface TestAnnotation {
    String value();
}

@interface TestAnnotations { // 여러 개의 TestAnnotation 애노테이션을 담은 컨테이너 애노테이션 
    TestAnnotation[] value(); // 이름이 반드시 value이어야 한다!!
}

 

위 코드를 보면 Test 클래스에 @TestAnnotation이 2번 붙은 것을 확인할 수 있다. 이때 TestAnnotation 애노테이션 타입을 잘 살펴보면 앞서 언급한 @Repeatable이 붙은 것을 확인할 수 있는데, 별도의 컨테이너 애노테이션이 지정되어 있다. 즉, 여러 개의 애노테이션을 붙일 경우 해당 컨테이너에 요소로 지정된 배열에 값들이 저장되는 것이다.

 

이때 주의할 점으로는 컨테이너 애노테이션의 Retention 정책과 Target 정책의 범위가 요소 애노테이션 보다 같거나 넓어야 한다.

 

애노테이션 프로세서란?

지금까지 애노테이션을 정의하는 방법과 메타 애노테이션의 종류에 대해 간단히 알아보았다. 그런데 자바에서 기본적으로 제공하는 애노테이션들은 '뭔가' 처리가 되었을테니 잘 동작하리라 짐작되지만, 우리가 어떤 의도를 가지고 직접 요소를 지정하고 메타 애노테이션을 붙임으로써 정의한 새로운 애노테이션은 우리가 의도한대로 어떻게 동작시킬 수 있을까?

 

예를 들어 보일러 플레이트 코드 작성(getter, setter, constructor 작성 등) 등 반복되는 작업을 줄이는데 도움을 주는 Lombok(롬복) 같은 라이브러리에서 제공하는 간편한 애노테이션을 어떻게 하면 우리가 직접 정의할 수 있을까?

 

이를 위해선 우선 애노테이션 프로세서에 대해 알아야 한다. 애노테이션 프로세서는 컴파일 단계에서 애노테이션이 적용된 코드 베이스를 검사하거나 수정 및 생성하는데 사용되는 것으로, (소스 코드뿐만 아니라 메타 데이터, 문서 리소스 등도 생성 가능) 애노테이션들을 탐색 및 처리하기 위한 자바 컴파일러 플러그인의 일종이라고 볼 수 있다.

 

개발자는 특정 애노테이션에 대해 자기 자신만의 애노테이션 프로세서를 등록할 수 있어 이를 적재적소에 잘 사용한다면 코드를 단순화할 수 있다. 즉, 자기 자신만의 롬복 라이브러리를 구현할 수도 있는 것이다. 애노테이션 프로세서는 Java 5에 처음 등장했지만, Java 6부터 유용한 API가 등장하여 활성화되었다. 이러한 애노테이션 프로세서는 롬복 등 많은 자바 라이브러리에서 사용된다.

 

※ 여기서는 애노테이션 프로세서에 대한 간단한 개념만 다루고, 애노테이션 프로세서를 직접 등록하는 작업은 추후에 다루고자 한다.

 

 

참고자료

  • 도우출판 "자바의 정석"
  • https://www.youtube.com/watch?v=G-e-FB3oFa4
  • https://www.charlezz.com/?p=1167 
  • https://docs.oracle.com/javase/tutorial/java/annotations/declaring.html