애노테이션(Annotation)

애노테이션은 소스 코드에 대한 메타데이터(metadata), 즉 다른 데이터를 설명해 주는 데이터를 제공합니다. 주석(comments)과 마찬가지로 코드를 실행하는데 있어서 직접적인 영향을 미치지는 않습니다. 클래스, 메서드, 변수, 매개변수 등에 애노테이션을 달 수 있으며, 애노테이션은 아래와 같이 문자 @로 시작합니다.

@Override
public void foo() {
    // ...
}

보통은 리플렉션과 같이 활용되며, 문서, 컴파일러 혹은 IDE, 테스트 프레임워크(JUnit, JCStress 등)나 ORM(Hibernate, JPA 등) 등과 같이 다양한 곳에서 상당히 유용하게 사용됩니다.

애노테이션의 위치

애노테이션이 들어갈 수 있는 위치는 상당히 다양합니다. 클래스, 인터페이스, 생성자, 메서드, 메서드의 매개변수, 필드, 지역 변수 선언에 애노테이션을 배치할 수 있습니다.

@Class
class MyClass {
    @Method
    public void foo(@Parameter int x) {
        @Local
        int t;
        // ...
    }
}

@Interface
class MyInterface {
    // ...
}

자바 7 이전에는 선언에만 애노테이션을 달 수 있었지만, 자바 8 이후부터는 타입에도 애노테이션을 달 수 있습니다. 즉, 타입이 등장하는 모든 곳에서 애노테이션을 달 수 있습니다. 이런 애노테이션을 따로 타입 애노테이션(type annotation)이라고 부르기도 합니다. 예를 들면 다음과 같습니다.

// 인스턴스화 시
new @Interned MyObject();

// 타입 캐스팅 시
myString = (@NonNull String)str;

// implements 절에서
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }

// throws 절에서
void monitorTemperature() throws @Critical TemperatureException { ... }

애노테이션의 종류

내장 애노테이션(Built-in annotations)

내장 애노테이션에는 아래에서 설명한 것 외에도 @FunctionalInterface, @SafeVarargs도 있지만 여기에서는 개발자 입장에서 자주 사용할 수 있는 내장 애노테이션 위주로 살펴보겠습니다.

@Override

부모 클래스의 메서드를 오버라이딩하고 있는지 확인합니다. 만약에 자신이 상속받거나 구현하고 있는 클래스나 인터페이스에서 메서드 시그니처와 반환형이 동일한 메서드를 찾을 수 없는 경우에는 컴파일 에러가 발생합니다.

class Parent {
    public void methodA(int x) {
        // ...
    }
}

class Child extends Parent {
    @Override
    public void methodA(int x) {
        // ...
    }
}

굳이 @Override 애노테이션을 붙이지 않아도 이상은 없지만, 붙이면 이 메서드가 다른 메서드를 오버라이딩하고 있음을 확인할 수 있다는 장점이 있습니다. 만약에 오버라이딩하고 있는 부모 클래스의 메서드 시그니처나 반환형이 변경된다면 컴파일러가 컴파일 에러를 발생시킵니다. 이를 통해서 개발자는 사소한 오타나 선언의 변경 등이 있을 때 빠르게 알아챌 수 있습니다.

@Deprecated

중요도가 떨어져 더 이상 사용되지 않고 앞으로는 사라지게 될 클래스, 메서드, 필드에 사용할 수 있습니다. 예를 들어서, 이 애노테이션이 붙은 메서드를 사용하려는 경우에는 컴파일러가 이 메서드는 더 이상 사용되지 않는다는 경고를 표시합니다. 이 애노테이션은 해당 코드를 작성한 개발자가 사용자에게 '이 메서드 대신에 더 안전하고 괜찮은 방법이 있다', '이 메서드는 버전이 올라가면서 사라질 예정이지만 이전 버전과의 호환성을 위해 아직 제거하지는 않았다'라고 말하는 것입니다.

class Foo {
    @Deprecated
    public void bar() {
        // ...
    }
}

public class AnnotationTest {
    public static void main(String[] args) throws Exception {
        Foo foo = new Foo();
        foo.bar(); // deprecated
    }
}

사용하고 있는 IDE에 따라서 경고를 내보내거나 메서드에 취소선을 긋는 방법으로 사용자에게 이를 알려줍니다. 이클립스에서 위와 같이 작성하면 아래와 같이 메서드명에 취소선이 그이는 것을 확인할 수 있습니다.

@SuppressWarnings

말 그대로 컴파일러가 발생시키는 경고를 무시할 때 사용합니다. 사용하고 있는 컴파일러와 IDE마다 지원하는 옵션들이 다릅니다. 이클립스를 사용하고 있는 개발자들은 이클립스(Eclipse) 문서에서 해당 목록을 확인해볼 수 있습니다. 경고를 무시하지 않고 해결하는 것이 보통이지만, 개발자가 '나는 이런 경고를 인지하고 있으며 이것이 내가 의도한 바이다'라는 걸 컴파일러에게 전하고 싶을 때 사용합니다.

예를 들어서, 아래와 같이 입력하면 개발자가 변수를 선언은 했지만 아직 사용하고 있진 않는 경우에 ''지역 변수의 값이 사용되지 않습니다."라는 경고를 무시할 수 있습니다.

class Foo {
    @SuppressWarnings("unused")
    public void bar() {
        int a;
    }
}

사용자 애노테이션(Custom annotations)

물론 개발자가 자신만의 애노테이션을 직접 만들 수도 있습니다. 아래와 같이 애노테이션을 선언할 때는 @interface 키워드를 사용합니다.

@interface MyAnnotation { }

이렇게 만든 애노테이션을 아래와 같이 달 수 있습니다. 애노테이션은 일종의 주석이므로 이름을 잘 지어두면 애노테이션을 붙인 개발자의 의도를 쉽게 파악할 수 있습니다.

class Foo {
    @MyAnnotation
    public void bar() { /* ... */ }
}

요소 추가하기

하지만 뭔가 부족합니다. 애노테이션에 뭔가 다른 요소들을 추가할 수 없을까요? 물론 가능하지만 알아두어야 할 몇 가지 규칙들이 있습니다. 아래를 보면 아시겠지만 마치 인터페이스를 선언할 때와 비슷합니다.

@interface MyAnnotation {
    int foo();
    String bar();
}

class Foo {
    @MyAnnotation(foo = 10, bar = "any string")
    public void bar() { /* ... */ }
}

애노테이션에 요소를 추가하고 싶을 때는 본문이 없는 메서드 선언을 추가하면 되지만, 아래의 규칙들을 지켜야 합니다.

  • 메서드 선언에는 매개변수를 사용할 수 없다.
  • 메서드 선언에는 throws절을 사용할 수 없다.
  • 메서드의 반환형에는 기본 타입, 문자열, 열거형, Class 클래스, 애노테이션 혹은 앞에서 언급한 타입의 1차원 배열만 올 수 있다.

위 코드에서는 애노테이션 요소의 값을 모두 적지 않으면 에러가 발생하는데, 기본값을 주고 싶다면 아래와 같이 default 키워드를 사용하고 그 뒤에 기본값을 적으면 됩니다. 어떤 요소의 값을 적지 않았을 때는 해당 요소의 기본값을 사용합니다.

@interface MyAnnotation {
    int foo();
    String bar() default "any string";
}

class Foo {
    // @MyAnnotation(foo = 10, bar = "any string")과 동일
    @MyAnnotation(foo = 10)
    public void bar() { /* ... */ }
}

단일 값 전달하기

하나의 값을 전달할 때는 요소명을 생략할 수 있습니다. 이때, 요소명은 value여야 하며 애노테이션은 하나의 요소만 가지고 있어야 합니다.

@interface MyAnnotation {
    // 반드시 요소명은 value여야 한다.
	int value();
}

@MyAnnotation(10) // 이와 같이 'value ='를 생략할 수 있다.
class Foo { /* ... */ }

사실 두 개 이상의 요소를 가질 수도 있지만, 이 경우에는 value 요소를 제외한 나머지 요소에 기본값이 있어야 합니다.

@interface MyAnnotation {
    int value();
    int otherElement() default 0;
}

여러 종류의 값 전달하기

배열을 전달할 때는 아래와 같이 배열 이니셜라이저({...})를 사용해서 전달할 수 있습니다.

@interface Foo {
	int[] value();
}

@Foo({1, 2, 3})
class Baz { /* ... */ }

애노테이션은 아래와 같은 방법으로 전달하면 됩니다.

@interface Foo {
	Bar[] value();
}

@interface Bar {
	String message();
}

@Foo({
	@Bar(message="lorem"),
	@Bar(message="ipsum")
})
class Baz { /* ... */ }

다양한 멤버 선언하기

간단히만 살펴보고 넘어갑시다. 애노테이션 내부에 요소 말고도 아래와 같이 또 다른 애노테이션을 선언하거나, 클래스, 인터페이스, 상수를 선언할 수도 있습니다.

@interface MyAnnotation {
	class SomeClass { } // 클래스 선언
	interface SomeInterface { } // 인터페이스 선언
	int SOME_CONSTANT = 10; // 상수 선언. 기본적으로 static final이다.
	@interface SomeAnnotation { // 애노테이션 선언
		// ...
	}
}

메타 애노테이션(Meta annotations)

애노테이션에 대한 애노테이션을 메타 애노테이션이라고 부릅니다. 이 역시 내장 애노테이션이지만 이 애노테이션들은 특별하기에 별도로 살펴보겠습니다.

@Retention

애노테이션을 언제까지 유지할 것인지를 지정할 수 있습니다.

RetentionPolicy.SOURCE

소스 코드에서만 애노테이션을 유지합니다. 즉, 컴파일 후에 생성되는 바이트코드(.class)에서는 애노테이션이 기록되지 않습니다. 이 애노테이션을 사용할 때는 바이트코드를 더럽히지 않고 코드 수준에서 검사해도 충분한 경우이며 컴파일러나 IDE 등의 도구가 이를 활용할 수 있습니다. 내장 애노테이션에서 예로 들면 @Override, @SuppressWarnings을 들 수 있습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@interface Encoding { /* ... */ }

@Encoding
final class MutableListEncoding<T> { /* ... */ }

RetentionPolicy.CLASS

컴파일러가 바이트코드(.class)에 애노테이션을 기록하지만, 런타임에는 JVM이 해당 애노테이션을 제거합니다. 기본값이지만 활용할 수 있는 범위가 제한적입니다. 프로그래밍 방식으로 바이트코드 수준에서 조작하거나 검사하는 경우 이를 활용할 수 있습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@interface UnstableApi { /* ... */ }

@UnstableApi
final class Allocation { /* ... */ }

디스어셈블러(javap)를 통해서 바이트코드에서 확인할 수는 있습니다.

Constant pool:
...
  #16 = Utf8               RuntimeInvisibleAnnotations
  #17 = Utf8               Labc/UnstableApi;
...
RuntimeInvisibleAnnotations:
  0: #17()

하지만 런타임에서 사라지므로 리플렉션 API로 애노테이션을 읽을 수는 없습니다. 리플렉션은 이곳에서 설명하며 내용이 길어지므로 여기에서는 설명하지 않습니다.

public class RetentionPolicyTest {
    public static void main(String[] args) {
        Class clazz = Allocation.class;
        Annotation[] annotations = clazz.getAnnotations();
        System.out.println(annotations.length); // = 0
    }
}

RetentionPolicy.RUNTIME

컴파일러가 바이트코드(.class)에 애노테이션을 기록하고, 런타임에도 JVM이 해당 애노테이션을 제거하지 않습니다. 따라서, 런타임에 리플렉션을 통해서 애노테이션을 읽을 수 있습니다. RetentionPolicy.CLASS가 기본값이지만 RetentionPolicy.RUNTIME을 주로 사용합니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Traced { /* ... */ }

@Traced
class BusinessComponent { /* ... */ }

디스어셈블러(javap)를 통해서 바이트코드에서 확인할 수 있습니다.

Constant pool:
...
  #16 = Utf8               RuntimeVisibleAnnotations
  #17 = Utf8               Labc/Traced;
...
RuntimeVisibleAnnotations:
  0: #17()

런타임에서도 유지되므로 리플렉션 API를 통해 애노테이션을 읽을 수도 있습니다.

public class RetentionPolicyTest {
    public static void main(String[] args) {
        Class clazz = BusinessComponent.class;
        Annotation[] annotations = clazz.getAnnotations();
        System.out.println(annotations.length); // = 1
    }
}

@Target

말 그대로 애노테이션을 사용할 수 있는 대상(target)입니다. @Target 애노테이션을 사용하지 않으면 기본적으로 애노테이션이 등장할 수 있는 모든 위치에서 사용할 수 있습니다.

@Retention(RetentionPolicy.RUNTIME)
// 이 애노테이션의 대상은 메서드이다. 다른 곳에 붙이려고 하면 에러가 발생한다.
@Target(ElementType.METHOD)
@interface Before { }

class A { 
    @Before
    public void foo(int x) {
        // ...
    }
}

위에서 볼 수 있는 ElementType.METHOD 말고도 다양한 열거형 상수들이 있습니다. 자바 16을 기준으로 아래와 같은 열거형 상수들을 확인할 수 있습니다.

@Inherited

말 그대로 애노테이션이 상속된다는 의미입니다. 아래 예시에서 @Traced 애노테이션은 A 클래스를 상속한 B 클래스에도 적용됩니다.

@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Traced { }

@Traced
class A { }

class B extends A { }

@Documented

말 그대로 자바독 문서에 기록됩니다. 예를 들어서 @Documented가 적용된 애노테이션을 사용하는 클래스가 있으면 해당 클래스의 자바독 문서에서 지정한 애노테이션을 확인할 수 있습니다.

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Filter { /* ... */ }

// 자바독 생성기는 자바독 문서에 @Filter 애노테이션을 표시한다.
@Filter
public class DocumentedAnnotationTest {
    // ...
}

생성된 자바독 문서에서 DocumentedAnnotationTest 문서를 찾아보면 아래와 같이 @Filter 애노테이션을 확인할 수 있습니다. 만약에 @Filter 애노테이션에 @Documented을 제외하면 자바독 문서에서 표시되지 않습니다.

@Repeatable

이 애노테이션은 자바 8 이후부터 사용할 수 있습니다. 이름에서 유추할 수 있듯이 @Repeatable이 붙지 않은 애노테이션은 한 번만 달 수 있습니다. 예를 들어서, @MyAnnotation에 @Repeatable이 없을 때는 아래와 같이 작성하면 애노테이션이 중복된다는 에러가 발생합니다.

@MyAnnotation
@MyAnnotation
public void foo() { /* ...*/ }

@Repeatable을 붙이면 여러 개의 애노테이션을 붙일 수 있습니다. 이때, @Repeatable의 인수에는 컨테이너 애노테이션의 클래스 타입을 전달합니다. 이 클래스 타입은 타입의 뒤에 .class를 붙여서 얻을 수 있습니다. 이때, 컨테이너 애노테이션에서는 메서드명이 value이고 반환형은 애노테이션의 배열 타입인 요소를 갖고 있어야 합니다. 컨테이너 애노테이션이 무엇인지는 아래 예시를 보면 쉽게 파악할 수 있습니다.

// 컨테이너 애노테이션. 여기서는 Tag를 담고 있는 그릇의 역할을 한다.
@interface Tags {
    // 반드시 반환형은 1차원 배열이어야 하며 메서드명은 value여야 한다.
    Tag[] value();
}

// 컨테이너 애노테이션의 클래스 타입을 인수로 넘긴다.
@Repeatable(Tags.class)
@interface Tag {
	String name();
}

// @Repeatable이 붙은 애노테이션은 여러 번 사용할 수 있다.
@Tag(name="foo")
@Tag(name="bar")
class Foo {
	public void bar(int x) { /* ... */ }
}

'프로그래밍 관련 > 자바' 카테고리의 다른 글

35편. 스트림(Streams) (1)  (0) 2022.04.04
32편. 람다식(Lambda expression)  (1) 2022.02.22
26편. 제네릭(Generic)  (0) 2022.02.06
25편. 중첩 클래스(Nested Class)  (1) 2022.02.06
29편. 스레드(Thread) (2)  (1) 2022.01.27