해당 글을 백기선 님의 자바 스터디 12주차 과제를 공부하고 공유하기 위해서 작성되었습니다.

목표

자바의 애노테이션에 대해 학습한다.

학습할 것

애노테이션 정의하는 방법

애노테이션(annotation)이란?

   애노테이션(annotation)의 뜻은 주석, 주해이다. 그렇다면 자바에서 사용하는 annotation이 무언가에 대한 주석이라는 것을 추측해볼 수 있다. 무엇에 대한 주석일까?

자바를 개발한 사람들은 소스코드에 대한 문서를 소스코드와 별개로 하기 보다는 하나의 파일로 관리하는 편이 좋다고 생각했다. 그래서 소스코드에 주석 /* ... */에 소스코드에 대한 정보를 저장하고, 주석으로부터 HTML을 생성해내는 프로그램 javadoc.exe을 만들어서 사용했다.

이처럼 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 애노테이션이다. 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서 유용한 정보를 제공하는 기능인 것이다.

   애노테이션은 JDK에서 기본적으로 제공하는 것과 다른 프로그램에서 제공하는 것들이 있는데, 어느 것이든 약속된 형식으로 정보를 제공하기만 하면 된다.

JDK에서 제공하는 표준 애노테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다. 그리고 새로운 애노테이션을 정의할 때 사용하는 메타 애노테이션을 제공한다.

💡 JDK에서 제공하는 애노테이션은 ‘java.lang.annotation’ 패키지에 포함되어 있다.

애노테이션 타입 정의하기

   애노테이션을 정의하는 방법은 간단하다. @ 기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일하다.

@interface 애노테이션이름 {
    타입 요소이름();  // 애노테이션의 요소를 선언.
      ...
}

💡 엄밀히 말해서 @Override는 애노테이션이고 Override는 애노테이션의 타입이다.

애노테이션의 요소

애노테이션 내에 선언된 메서드를 애노테이션의 요소(element)라고 한다. 애노테이션에도 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메서드는 정의할 수 없다.

▶ 애노테이션 요소의 규칙

애노테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙이 있다.

  • 요소의 타입은 기본형, String, enum, 애노테이션, Class만 허용된다.
  • ( )안에 매개변수를 선언할 수 없다.
  • 예외를 선언할 수 없다.
  • 요소를 타입 매개변수로 정의할 수 없다.

애노테이션의 요소는 반환값이 있고 매개변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다.

package me.gracenam.study.week12;

public @interface AnnoEx_01 {
    int num();
    String str();
    String[] strArr();
}  

애노테이션의 각 요소는 기본값을 가질 수 있으며, 기본값이 있는 요소는 애노테이션을 적용할 때 값을 지정하지 않으면 기본값이 사용된다. 이 때 기본값으로 null을 제외한 모든 리터럴이 사용된다.

@interface TestAnno {
    int count() default 1;
}

@TestAnno   // @TestAnno(count = 1)과 동일
public class NewClass { ... }

애노테이션 요소가 하나뿐이고 이름이 value인 경우, 애노테이션을 적용할 때 요소의 이름을 생략하고 값만 적어도 된다.

@interface TestAnno {
    String value();
}

@TestAnno("java") // @TestAnno(value = "java")
class NewClass { ... }

요소의 타입이 배열인 경우, 괄호 { }를 사용해서 여러 개의 값을 지정할 수 있다. 기본값을 지정할 때도 괄호 { }를 사용할 수 있다.

@interface TestAnno {
    String[] strArr();
}

@Test(strArr = {"One", "Two"})  // 값이 여러 개인 경우
@Test(strArr = "Three")         // 값이 하나일 때는 괄호 생략 가능
@Test(strArr = {})              // 값이 없을 때는 생략 불가능
@interface TestAnno {
    String[] strArr_1() default {"A is a", "B is b"};   // 기본 값이 여러 개인 경우
    String[] strArr_2() default "C is c";               // 기본 값이 하나인 경우
}

@TestAnno
class NewClass { ... }

요소의 타입이 배열일 때도 이름이 value이면, 이름을 생략할 수 있다.

마커 애노테이션 Marker Annotation

   값을 지정할 필요가 없는 경우, 애노테이션의 요소를 하나도 정의하지 않을 수 있다. 이렇게 요소가 하나도 정의되지 않은 애노테이션을 마커 애노테이션이라고 한다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Test { }    // 마커 애노테이션. 정의된 요소가 하나도 없다.

java.lang.annotation.Annotation

   모든 애노테이션의 조상은 Annotation이다. 그러나 애노테이션은 상속이 허용되지 않으므로 명시적으로 조상을 지정할 수 없다.

@interface TestAnno extends Annotation {    // 에러. 허용되지 않은 표현.
    ...
}

또한 Annotation은 애노테이션이 아닌 일반 인터페이스로 정의되어 있다.

package java.lang.annotation;

/**
 * The common interface extended by all annotation types.  Note that an
 * interface that manually extends this one does <i>not</i> define
 * an annotation type.  Also note that this interface does not itself
 * define an annotation type.
 *
 * More information about annotation types can be found in section 9.6 of
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * The {@link java.lang.reflect.AnnotatedElement} interface discusses
 * compatibility concerns when evolving an annotation type from being
 * non-repeatable to being repeatable.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
public interface Annotation {
    /**
     * Returns true if the specified object represents an annotation
     * that is logically equivalent to this one.  In other words,
     * returns true if the specified object is an instance of the same
     * annotation type as this instance, all of whose members are equal
     * to the corresponding member of this annotation, as defined below:
     * <ul>
     *    <li>Two corresponding primitive typed members whose values are
     *    {@code x} and {@code y} are considered equal if {@code x == y},
     *    unless their type is {@code float} or {@code double}.
     *
     *    <li>Two corresponding {@code float} members whose values
     *    are {@code x} and {@code y} are considered equal if
     *    {@code Float.valueOf(x).equals(Float.valueOf(y))}.
     *    (Unlike the {@code ==} operator, NaN is considered equal
     *    to itself, and {@code 0.0f} unequal to {@code -0.0f}.)
     *
     *    <li>Two corresponding {@code double} members whose values
     *    are {@code x} and {@code y} are considered equal if
     *    {@code Double.valueOf(x).equals(Double.valueOf(y))}.
     *    (Unlike the {@code ==} operator, NaN is considered equal
     *    to itself, and {@code 0.0} unequal to {@code -0.0}.)
     *
     *    <li>Two corresponding {@code String}, {@code Class}, enum, or
     *    annotation typed members whose values are {@code x} and {@code y}
     *    are considered equal if {@code x.equals(y)}.  (Note that this
     *    definition is recursive for annotation typed members.)
     *
     *    <li>Two corresponding array typed members {@code x} and {@code y}
     *    are considered equal if {@code Arrays.equals(x, y)}, for the
     *    appropriate overloading of {@link java.util.Arrays#equals}.
     * </ul>
     *
     * @return true if the specified object represents an annotation
     *     that is logically equivalent to this one, otherwise false
     */
    boolean equals(Object obj);

    /**
     * Returns the hash code of this annotation, as defined below:
     *
     * <p>The hash code of an annotation is the sum of the hash codes
     * of its members (including those with default values), as defined
     * below:
     *
     * The hash code of an annotation member is (127 times the hash code
     * of the member-name as computed by {@link String#hashCode()}) XOR
     * the hash code of the member-value, as defined below:
     *
     * <p>The hash code of a member-value depends on its type:
     * <ul>
     * <li>The hash code of a primitive value <i>{@code v}</i> is equal to
     *     <code><i>WrapperType</i>.valueOf(<i>v</i>).hashCode()</code>, where
     *     <i>{@code WrapperType}</i> is the wrapper type corresponding
     *     to the primitive type of <i>{@code v}</i> ({@link Byte},
     *     {@link Character}, {@link Double}, {@link Float}, {@link Integer},
     *     {@link Long}, {@link Short}, or {@link Boolean}).
     *
     * <li>The hash code of a string, enum, class, or annotation member-value
     I     <i>{@code v}</i> is computed as by calling
     *     <code><i>v</i>.hashCode()</code>.  (In the case of annotation
     *     member values, this is a recursive definition.)
     *
     * <li>The hash code of an array member-value is computed by calling
     *     the appropriate overloading of
     *     {@link java.util.Arrays#hashCode(long[]) Arrays.hashCode}
     *     on the value.  (There is one overloading for each primitive
     *     type, and one for object reference types.)
     * </ul>
     *
     * @return the hash code of this annotation
     */
    int hashCode();

    /**
     * Returns a string representation of this annotation.  The details
     * of the representation are implementation-dependent, but the following
     * may be regarded as typical:
     * <pre>
     *   &#064;com.acme.util.Name(first=Alfred, middle=E., last=Neuman)
     * </pre>
     *
     * @return a string representation of this annotation
     */
    String toString();

    /**
     * Returns the annotation type of this annotation.
     * @return the annotation type of this annotation
     */
    Class<? extends Annotation> annotationType();
}

한 가지 특이한 점은 상속이 안됨에도 불구하고 컴파일한 후 바이트 코드를 보면 상속이 되어 있다!

package me.gracenam.study.week12;

public @interface AnnoEx_01 {
    int num();
    String str();
    String[] strArr();
}  


표준 애노테이션

   자바에서 기본적으로 제공하는 애노테이션들은 몇 개 없다. 심지어 일부는 메타 애노테이션(meta annotation)으로 애노테이션을 정의하는데 사용되는 애노테이션의 애노테이션이다.

▶ 자바에서 기본적으로 제공하는 표준 애노테이션

애노테이션 설명
@Override 컴파일러에게 오버라이딩하는 메서드라는 것을 알린다.
@Deprecated 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다.
@SuppressWarnings 컴파일러의 특정 경고메시지가 나타나지 않게 해준다.
@SafeVarargs 지네릭스 타입의 가변인자에 사용한다.(JDK1.7)
@FunctionalInterface 함수형 인터페이스라는 것을 알린다.(JDK1.8)
@Native native메서드에서 참조되는 상수 앞에 붙인다.(JDK1.8)
@Target M 애노테이션이 적용가능한 대상을 지정하는데 사용한다.
@Documented M 애노테이션 정보가 javadoc으로 작성된 문서에 포함되게 한다.
@Inherited M 애노테이션이 자손 클래스에 상속되도록 한다.
@Retention M 애노테이션이 유지되는 범위를 지정하는데 사용한다.
@Repeatable M 애노테이션을 반복해서 적용할 수 있게 한다.(JDK1.8)


M가 붙은 애노테이션들은 메타 애노테이션이다.

@Override

메서드 앞에서만 붙일 수 있는 애노테이션으로, 조상의 메서드를 오버라이딩하는 것이라는 사실을 컴파일러에게 알려주는 역할을 한다.

@Override를 붙이면 컴파일러가 같은 이름의 메서드가 조상에 있는지 확인해 주기 때문에 실수를 방지할 수 있다.

@Deprecated

새로운 버전의 JDK가 소개될 때, 새로운 기능이 추가되면서 기존의 부족했던 기능이 개선되기도 한다. 이 과정에서 대체할 수 있는 새로운 기능이 추가되어도 여러 곳에서 사용되고 있는 기존의 기능을 함부로 삭제할 수는 없다.

@Deprecated는 더 이상 사용되지 않는 필드나 메서드에 붙이는 애노테이션이다. 이 애노테이션이 붙은 대상은 다른 것으로 대체되었으니 더 이상 사용하지 않는 것을 권한다는 의미이다.

@FunctionalInterface

함수형 인터페이스(function interface)를 선언할 때, 이 애노테이션을 붙이면 컴파일러가 함수형 인터페이스를 올바르게 선언했는지 확인하고, 잘못된 경우 에러를 발생시킨다. 필수는 아니지만, 붙이면 실수를 방지할 수 있으므로 사용하는 것을 권장한다.

@SuppressWarnings

컴파일러가 보여주는 경고메시지가 나타나지 않도록 억제해주는 애노테이션이다. 컴파일러의 경고메시지는 무시하고 넘어갈 수도 있지만, 확인하고 해결해서 컴파일 후에 어떠한 메시지도 나타나지 않도록 하는 것이 좋다.

그러나 경우에 따라 경고가 발생할 것을 알지만 그냥 놔둘 때가 있는데, 이럴 경우 컴파일을 할 때마다 메시지를 보게 된다. 이럴 때 경고가 발생하는 대상에 @SuppressWarnings을 붙여서 경고 메시지가 나타나지 않게 할 수 있다.

@SuppressWarnings로 억제할 수 있는 경고 메시지의 종류는 다양한데, JDK 버전이 올라가면서 계속 추가될 것이다. 이 중 주로 사용되는 것은 deprecation, unchecked, rawtypes, varargs 정도이다.

메타 애노테이션

   메타 애노테이션은 애노테이션을 위한 애노테이션, 즉 애노테이션에 붙이는 애노테이션으로 애노테이션을 정의할 때 애노테이션의 적용대상(target)이나 유지기간(retention)등을 지정하는데 사용된다. java.lang.annotation 패키지에 포함되어 있다.

@retention

애노테이션이 유지(retention)되는 기간을 지정하는데 사용된다.

▶ 애노테이션의 유지 정책(retention policy)

애노테이션은 사용 용도에 따라 @AnnotationName을 어느 범위까지 유지할 것인지 지정해야 한다. 이러한 유지정책은 java.lang.annotation.RetentionPolicy 열거 상수로 아래와 같이 정의된다.

유지 정책 의미
SOURCE 소스 파일에만 존재. 클래스파일에는 존재하지 않음.
CLASS 클래스 파일에 존재. 실행시에 사용불가. 기본값
RUNTIME 클래스 파일에 존재. 실행시에 사용가능.

SOURCE

@Override@SuppressWarnings처럼 컴파일러가 사용하는 애노테이션은 유지 정책이 ‘SOURCE’이다. 컴파일러를 직접 작성할 것이 아니면, 이 유지정책은 필요하지 않다.

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override { }

RUNTIME

유지 정책을 ‘RUNTIME’으로 하면, 실행 시에 리플렉션(reflection)을 통해 클래스 파일에 저장된 애노테이션의 정보를 읽어서 처리할 수 있다.

❔ 리플렉션(reflection)이란?

구체적인 클래스 타입을 알지 못해도 해당 클래스의 객체 생성, 메소드, 타입, 변수들에 접근할 수 있도록 도와주는 Java API를 말한다.

더 자세한 내용은 선장님의 더 자바 - 코드를 조작하는 다양한 방법을 참고하자.

▶ 예제

package me.gracenam.study.week12;

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

public class AnnoEx_02 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class cls = Class.forName("me.gracenam.study.week12.AnnoEx_02$ReflectionClass");
        Annotation[] annotations = cls.getDeclaredAnnotations();
        for(Annotation annotation : annotations) {
            System.out.println("사용된 annotation : " + annotation.toString());
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    @interface MyAnnotation01 {}

    @MyAnnotation01
    class ReflectionClass {}
}


@FunctionalInterface@Override처럼 컴파일러가 체크해주는 애노테이션이지만, 실행 시에도 사용되므로 유지정책이 ‘RUNTIME’으로 되어 있다.

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

CLASS

유지 정책 ‘CLASS’는 컴파일러가 애노테이션의 정보를 클래스 파일에 저장할 수는 있게 하지만, 클래스 파일이 JVM에 로딩될 때는 애노테이션의 정보가 무시되어 실행 시에 애노테이션에 대한 정보를 얻을 수 없다. 이러한 이유 때문에 ‘CLASS’가 유지정책의 기본 값임에도 불구하고 잘 사용되지 않는다.

@target

애노테이션이 적용가능한 대상을 지정하는데 사용한다. 여러 개의 값을 지정할 때는 배열에서처럼 괄호{ }를 사용해야 한다.

@Target으로 지정할 수 있는 애노테이션 적용대상의 종류

대상 타입 의미
ANNOTATION_TYPE 애노테이션
CONSTRUCTOR 생성자
FIELD 필드(멤버변수, enum 상수)
LOCAL_VARIABLE 지역변수
METHOD 메서드
PACKAGE 패키지
PARAMETER 매개변수
TYPE 타입(클래스, 인터페이스, enum)
TYPE_PARAMETER 타입 매개변수(JDK 1.8)
TYPE_USE 타입이 사용되는 모든 곳(JDK 1.8)


‘TYPE’은 타입을 선언할 때 애노테이션을 붙일 수 있다는 뜻이고, ‘TYPE_USE’는 해당 타입의 변수를 선언할 때 붙일 수 있다는 뜻이다. 위 표에 나오는 값들은 java.lang.annotation.ElementType라는 열거형에 정의되어 있다.

@documented

@Documented는 애노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록한다. 자바에서 제공하는 기본 애노테이션 중에서 @Override@SuppressWarnings을 제외하면 모두 @Documented가 붙어 있다.

애노테이션 프로세서

어노테이션 프로세서는 컴파일 타임에 어노테이션을 스캔해서 처리하는 javac에 속한 빌드툴(hook)이다. 예를 들자면 롬복(lombok) 라이브러리가 있다.

  • Lombok
  • AutoService : java.util.ServiceLoader용 파일 생성 유틸리티
  • @Override

애노테이션 프로세서 장점

  • 애노테이션 프로세서는 애플리케이션을 구동하는 런타임 시점이 아니라, 컴파일 시점에 조작하여 사용함으로 런타임에 대한 비용이 제로가 된다.

애노테이션 프로세서 단점

  • 롬복 같은 경우 기존 코드를 변경하는 방법이지만 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작하기 때문에 해킹에 가깝다고 할 수 있다.

애노테이션 프로세서를 직접 작성하거나 혹은 만들어진 애노테이션 프로세서를 확인하면 한 가지 알 수 있는 사실은 AbstractProcessor 클래스를 상속받는 것이다.

package javax.annotation.processing;

import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import java.util.Objects;
import javax.lang.model.element.*;
import javax.lang.model.SourceVersion;
import javax.tools.Diagnostic;

/**
 * An abstract annotation processor designed to be a convenient
 * superclass for most concrete annotation processors.  This class
 * examines annotation values to compute the {@linkplain
 * #getSupportedOptions options}, {@linkplain
 * #getSupportedAnnotationTypes annotation types}, and {@linkplain
 * #getSupportedSourceVersion source version} supported by its
 * subtypes.
 *
 * <p>The getter methods may {@linkplain Messager#printMessage issue
 * warnings} about noteworthy conditions using the facilities available
 * after the processor has been {@linkplain #isInitialized
 * initialized}.
 *
 * <p>Subclasses are free to override the implementation and
 * specification of any of the methods in this class as long as the
 * general {@link javax.annotation.processing.Processor Processor}
 * contract for that method is obeyed.
 *
 * @author Joseph D. Darcy
 * @author Scott Seligman
 * @author Peter von der Ah&eacute;
 * @since 1.6
 */
public abstract class AbstractProcessor implements Processor {
    /**
     * Processing environment providing by the tool framework.
     */
    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    /**
     * Constructor for subclasses to call.
     */
    protected AbstractProcessor() {}

      ...
}

Reference