어노테이션

메서드를 오버라이딩 할때 사용했던 @Override와 같이 @ 기호를 사용하는 문법 요소로 Java5부터 등장했다.

단어의 의미인 주석과는 비슷하지만 다른 역할로써 사용되는데 메서드/클래스 등에 의미를 단순히 컴파일러에게 알려주기 위한 표식이 아닌 컴파일타임 이나 런타임에 해석될 수 있다.


1. 장점

기존의 자바는 선언적 프로그래밍방식으로 개발을 하면서 각 계층별 설정 데이터들을 XML에 명시했었는데 서비스의 규모가 클 수록 설정양이 많아지고 도메인 처리의 데이터들이 분산되어 있어 수정이 힘들었다.

어노테이션이 등장하면서 데이터 유효성 검사 등 직접 클래스에 명시해 줄 수 있게되어 수정이 필요할때 쉽게 파악할 수 있게 되었고 어토테이션의 재사용도 가능해졌다.

AOP(관점 지향 프로그래밍)을 쉽게 구성할 수 있게 해준다.


2. 용도

크게 문서화, 컴파일러 체크, 코드 분석과 자동 생성,런타임 프로세싱 용도로 사용될 수 있다.
컴파일 타임에 에러를 발생 시켜 경고하는 목적으로 사용될 수 있고 문서화는 컴파일 시 어노테이션이 붙은 데이터를 수집하여 가능하지만 가장 비중이 낮은 사용법이다.

유효성 검사와 같은 메타데이터로써 사용되고 reflection을 이용하여 특정 클래스를 주입할 수도 있다.

메타 데이터

“어떤 목적을 가지고 만들어진 데이터” -Karen Coyle-
한마디로 어떤 데이터를 설명해주는 데이터

Reflection

반사,투영이 라는 뜻으로 객체를 통해 클래스의 정보를 분석해내는 기법
ClassName, SuperClass, Constructors, Methods, Fields, Annotations …


3. 사용 방법

    @Override
    public ListNode remove(int position) {
        //생략...
    }
    //...

    @Test
    void removeTest() {
        //생략...
    }

사용할 클래스, 메서드, 매개변수등 앞에 붙여주기만 하면 된다.


4. 분류

  • Maker 어노테이션 : 멤버 변수가 없고 컴파일러에게 의미를 전달하기 위한 표식으로 사용되는 어노테이션 (ex. @Override )

  • Single-value 어노테이션 : 멤버로 단일변수를 갖고 데이터를 전달할 수 있는 어노테이션

  • Full 어노테이션 : 둘 이상의 변수를 갖는 어노테이션으로 데이터를 키 = 값형태로 전달한다.



빌트인 어노테이션

Java에 내장되어 있는 어노테이션으로 컴파일러를 위한 어노테이션

1. Override

현재 메서드가 슈퍼 클래스의 메서드를 오버라이드한 것임을 컴파일러에게 명시해준다.

메서드가 슈퍼클래스에 없다면 에러를 발생시기 때문에 오타와 같은 실수도 잡을 수 있다.


2. Deprecated

마커 어노테이션으로 다음 버전에 지원되지 않을 수도 있기 때문에 앞으로 사용하지 말라고 경고를 알린다.

Deprecated @Deprecated를 붙인 메서드는 IntelliJ에서 사진과 같이 알림을 띄워준다.


3. SuppressWarning

경고를 제거하는 어노테이션으로 개발자가 의도를 가지고 설계를 했는데 컴파일은 이를 알지 못하고 컴파일 경고를 띄울 수 있기 때문에 이를 제거하는 목적이다.


4. SafeVarargs

Java 7이상에서 사용가능하고 제네릭같은 가변인자 매개변수 사용시 경고를 무시한다

제네릭

사용할 클래스,메서드 내부에서의 데이터타입을 외부에서 지정하는 기법


5. FunctionalInterface

Java 8이상에서 사용가능하고 컴파일러에게 함수형 인터페이스라는 것을 알리는 어노테이션이다.

함수형 인터페이스

1개의 추상 메서드만을 갖고 있는 인터페이스로 10주차에 배운 Runnable이 그 예이다.



메타 어노테이션

어노테이션에 사용되는 어노테이션으로 어노테이션을 정의(설명)하기 위해 사용된다.

1. @Retention

어노테이션이 유지되는 기간(Life Time)을 설정하는 어노테이션

public enum RetentionPolicy {
    SOURCE,
    CLASS,
    RUNTIME
}
  • SOURCE : 소스파일에만 존재하고, 클래스파일에는 존재x, 컴파일러에 의해 버려진다.

  • CLASS : 클래스파일에는 존재하지만 런타임 시에 유지할 필요 없다는 것을 알리고 이 값이 default이다.

  • RUNTIME : 클래스파일에도 존재하고 런타임애 VM에 의해 유지되어 리플랙션을 통해 클래스 파일의 정보를 읽어 처리 가능하다.


2. @Target

어노테이션이 적용가능한 대상(동작 대상)을 지정한다.

만약 다른 타입이 온다면 컴파일 에러를 띄운다.

아래와 같은 ElmentType이라는 enum을 통해 지정한다. ( @Target(ElemntType.~)와 같이 사용 )


public enum ElementType {
    TYPE,
    FIELD,
    METHOD,
    PARAMETER,
    CONSTRUCTOR,
    LOCAL_VARIABLE,
    ANNOTATION_TYPE,
    PACKAGE,
    TYPE_PARAMETER,
    TYPE_USE,
    MODULE,

    @jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.RECORDS,essentialAPI=true)
    RECORD_COMPONENT;
}
  • 기존

    • TYPE : Class, Interface(어노테이션 타입 포함), enum, jdk14에 생긴 record

    • FIELD : 필드 값(프로퍼티), enum 상수값

    • METHOD : 메서드

    • PARAMETER : 메서드 파라미터 (매개 변수)

    • CONSTRUCTOR : 생성자

    • LOCAL_VARIABLE : 지역 변수

    • ANNOTATION_TYPE : 어노테이션

    • PACKAGE : 자바 패키지

  • jdk 1.8 이후 추가

    • TYPE_PARAMETER : 위의 어노테이션들과는 다르게 선언 주석이 아닌 TYPE 주석이다.

      • 선언 주석 : 주의사항, 사용방법, 사용처 등 설명
      • TYPE 주석 : 정수 값이 0보다 커야 한다, null이 아니다 와 같은 값에 대한 정보 제공함으로써 implements, thorws, new 구절에 사용하거나 제네릭에 사용함으로써 외부의 프로젝트에도 적용할 수 있도록 확장한 범위

      타입 선언부에 사용이 가능

      //annotation
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE_PARAMETER)
      public @interface ParmeterEx {
      }
      
      //class에 사용
      public class AnnotationStudy <@ParmeterEx T> {
      public void print( T t){}
      }
      
      //method에 사용
      public class AnnotationStudy {
      public <@ParmeterEx T> void print( T t){}
      }
      
      //method에 사용
      public class AnnotationStudy {
      public <@ParmeterEx T> void print(T t) throws @ParameterEx SomthingException{}
      }
      
      //BYTE CODE
      public class study/AnnotationStudy {
      // compiled from: AnnotationStudy.java
      
      @Lstudy/ParmeterEx;() : CLASS_TYPE_PARAMETER 0, null
      
      // access flags 0x0
      Ljava/lang/String; name
      
      // access flags 0x1
      public <init>()V
      L0
          LINENUMBER 3 L0
          ALOAD 0
          INVOKESPECIAL java/lang/Object.<init> ()V
      L1
          LINENUMBER 4 L1
          ALOAD 0
          LDC "default"
          PUTFIELD study/AnnotationStudy.name : Ljava/lang/String;
          RETURN
      L2
          LOCALVARIABLE this Lstudy/AnnotationStudy; L0 L2 0
          // signature Lstudy/AnnotationStudy<TT;>;
          // declaration: this extends study.AnnotationStudy<T>
          MAXSTACK = 2
          MAXLOCALS = 1
      
      // access flags 0x1
      // signature <R:Ljava/lang/Object;>()V
      // declaration: void print<R>()
      public print()V
      @Lstudy/ParmeterEx;() : METHOD_TYPE_PARAMETER 0, null
      

      컴파일하면서 해당 타입이 무슨 타입인지 분석하여 CLASS_TYPE_PARAMETER / METHOD_TYPE_PARAMETER 로 변환하는 것을 볼 수 있다.

      타입 선언부에 사용이 가능한 Target이므로 아래와 같이 실제 사용하는 부분에는 적용할 수 없다.

      public class AnnotationStudy {
      public <T> void print(@ParmeterEx T a){}
      }
      
    • MODULE : 모듈

  • jdk 1.9 이후 추가

    • TYPE_USE : 선언부 뿐만이 아닌 타입 사용되는 모든곳에 적용이 가능(클래스/인터페이스/내부필드/파라미터/제네릭/지역변수 등)

      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE_USE)
      public @interface ParmeterEx {
      }
      
      @ParmeterEx
      public class AnnotationStudy <@ParmeterEx T>{
          @ParmeterEx
          String name = "default";
      
          @NonNull
          public <@ParmeterEx R> void print(@ParmeterEx String t, @ParmeterEx R r){
              @ParmeterEx
              int a=1;
          }
      }
      
      //byte code
      // class version 61.0 (61)
      // access flags 0x21
      // signature <T:Ljava/lang/Object;>Ljava/lang/Object;
      // declaration: study/AnnotationStudy<T>
      public class study/AnnotationStudy {
      
      // compiled from: AnnotationStudy.java
      
      @Lstudy/ParmeterEx;()
      
      @Lstudy/ParmeterEx;() : CLASS_TYPE_PARAMETER 0, null
      
      // access flags 0x0
      Ljava/lang/String; name
      @Lstudy/ParmeterEx;() : FIELD, null
      
      // access flags 0x1
      public <init>()V
      L0
          LINENUMBER 4 L0
          ALOAD 0
          INVOKESPECIAL java/lang/Object.<init> ()V
      L1
          LINENUMBER 5 L1
          ALOAD 0
          LDC "default"
          PUTFIELD study/AnnotationStudy.name : Ljava/lang/String;
          RETURN
      L2
          LOCALVARIABLE this Lstudy/AnnotationStudy; L0 L2 0
          // signature Lstudy/AnnotationStudy<TT;>;
          // declaration: this extends study.AnnotationStudy<T>
          MAXSTACK = 2
          MAXLOCALS = 1
      
      // access flags 0x1
      // signature <R:Ljava/lang/Object;>(Ljava/lang/String;TR;)V
      // declaration: void print<R>(java.lang.String, R)
      public print(Ljava/lang/String;Ljava/lang/Object;)V
      @Lstudy/ParmeterEx;() : METHOD_TYPE_PARAMETER 0, null
      @Lstudy/ParmeterEx;() : METHOD_FORMAL_PARAMETER 0, null
      @Lstudy/ParmeterEx;() : METHOD_FORMAL_PARAMETER 1, null
      L0
          LINENUMBER 10 L0
          ICONST_1
          ISTORE 3
      L1
          LINENUMBER 11 L1
          RETURN
      L2
          LOCALVARIABLE this Lstudy/AnnotationStudy; L0 L2 0
          // signature Lstudy/AnnotationStudy<TT;>;
          // declaration: this extends study.AnnotationStudy<T>
          LOCALVARIABLE t Ljava/lang/String; L0 L2 1
          LOCALVARIABLE r Ljava/lang/Object; L0 L2 2
          // signature TR;
          // declaration: r extends R
          LOCALVARIABLE a I L1 L2 3
          LOCALVARIABLE @Lstudy/ParmeterEx;() : LOCAL_VARIABLE, null [ L1 - L2 - 3 ]
          MAXSTACK = 1
          MAXLOCALS = 4
      
      // access flags 0x9
      public static main([Ljava/lang/String;)V
      L0
          LINENUMBER 15 L0
          RETURN
      L1
          LOCALVARIABLE args [Ljava/lang/String; L0 L1 0
          MAXSTACK = 0
          MAXLOCALS = 1
      }
      
      

      바이트 코드를 보면 TYPE_USE를 사용해도 컴파일 결과는 컴파일러가 적절한 어노테이션 TARGET으로 바꾸는 것을 볼 수 있다.

      @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})
      @Retention(RetentionPolicy.CLASS)
      @Documented
      public @interface NonNull {
      }
      

      Lombok의 NonNull 어노테이션으로 해당 어노테이션도 Target범위가 TYPE_USE가 포함되어 있다.

  • jdk 14이후 추가

    • RECORD_COMPONENT : Record 컴포넌트



3. @documented

어노테이션의 정보가 javadoc의 문서에 포함되도록 하는 어노테이션


4. @Inherited

자식 클래스에게도 어노테이션이 상속되도록 하는 어노테이션


5. @Repeatable

어노테이션을 반복적으로 선언할 수 있게 하는 어노테이션



Java8 이후 추가된 특징

1. 중복 사용 가능

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface Chicken {
    String value() default "후라이드";
}

//error
@Chicken
@Chicken("양념")
public class App() {

}

기존에는 위와 같이 같은 어노테이션을 같은범위에 중복해서 정의할 수 없었는데 java8부터는 @Repeatable()이 추가되어 중복해서 사용이 가능해졌다.

Reapetable은 한개의 value를 가지고 있는데 여기에 일종의 어노테이션 컨테이너 역할을 할 어노테이션 클래스를 넘겨주면 해당 컨테이너에 중복사용한 어노테이션들을 담는 방식으로 동작하게 된다. 이때 주의할 점이 Reapeatable의 value는 어노테이션의 컨테이너 역할이기 때문에 중복해서 사용할 어노테이션보다 생명주기(RetentionPolicy)가 길어야만 한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface ChickenContainer {
    Chicken[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
@Repeatable(ChickenContainer.class)
public @interface Chicken {
    String value() default "후라이드";
}

@Chicken("양념")
@Chicken("마늘간장")
public class App {

    public static void main(String[] args) {
        ChickenContainer chickenContainer = App.class.getAnnotation(ChickenContainer.class);
        Arrays.stream(chickenContainer.value()).forEach(c -> {
            System.out.println(c.value());
        });
    }
}

//print
양념
마늘간장

//bytecode
// class version 59.0 (59)
// access flags 0x21
public class javaStudy/Example {

  // compiled from: Example.java

  @LjavaStudy/ChickenContainer;(value={@LjavaStudy/Chicken;(value="\uc591\ub150"), @LjavaStudy/Chicken;(value="\ub9c8\ub298\uac04\uc7a5")})
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LjavaStudy/Example; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 9 L0
    LDC LjavaStudy/Example;.class
    LDC LjavaStudy/ChickenContainer;.class
    INVOKEVIRTUAL java/lang/Class.getAnnotation (Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
    CHECKCAST javaStudy/ChickenContainer
    ASTORE 1
   L1
    LINENUMBER 10 L1
    ALOAD 1
    INVOKEINTERFACE javaStudy/ChickenContainer.value ()[LjavaStudy/Chicken; (itf)
    INVOKESTATIC java/util/Arrays.stream ([Ljava/lang/Object;)Ljava/util/stream/Stream;
    INVOKEDYNAMIC accept()Ljava/util/function/Consumer; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      (Ljava/lang/Object;)V,
      // handle kind 0x6 : INVOKESTATIC
      javaStudy/Example.lambda$main$0(LjavaStudy/Chicken;)V,
      (LjavaStudy/Chicken;)V
    ]
    INVOKEINTERFACE java/util/stream/Stream.forEach (Ljava/util/function/Consumer;)V (itf)
   L2
    LINENUMBER 13 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE chickenContainer LjavaStudy/ChickenContainer; L1 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x100A
  private static synthetic lambda$main$0(LjavaStudy/Chicken;)V
   L0
    LINENUMBER 11 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    INVOKEINTERFACE javaStudy/Chicken.value ()Ljava/lang/String; (itf)
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE c LjavaStudy/Chicken; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

ChickenConatiner 어노테이션을 생성하여 Chicken 어노테이션들을 value로 할당하는 것을 볼 수 있다.

MethodHandles$Lookup라는 이름의 클래스로 innerClass가 만들어지는 것을 볼 수 있는데 이는 메서드 핸들을 생성하기위한 factory class로 ChickenContainer의 value()인 Chicken[]에 접근하기위한 class



커스텀 어노테이션

annotation

IntelliJ의 새로만들기에서 Annotation으로 지정하여 새로 만들수 있다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomeAnnotation {
    String name() default "홍길동";
}

만들게 되면 @interface의 형태로 만들어지고 위의 메타 어노테이션을 붙여 메타 데이터를 표시할 수도 있다.

@interface 안에 매개변수가 없다면 Maker, 코드와 같이 한개만 존재한다면 Single-value, 두개이상을 갖는다면 Full어노테이션으로 구분할 수 있고 매개변수의 default값을 다음과 같이 지정해줄 수 도 있다.

package javaStudy.CustomAnnotation;

public class AnnotationExClass {
    @CustomAnnotation
    private String defaultName;

    @CustomAnnotation(name="철수")
    private String customName;

    public AnnotationExClass() {
        this.defaultName = "이름없음";
        this.customName = "이름없음";
    }

    public String getDefaultName() {
        return defaultName;
    }

    public String getName2() {
        return customName;
    }
}

다음과 같이 매개변수 하나는 default로 길동을 하나는 철수를 어노테이션 값으로 갖게하고 생성자로 매개변수의 값은 이름없음으로 생성하는 클래스가 있다고 한다면 아래와 같이 reflect를 활용해서 어노테이션 값에 접근할 수 있다.

    @Test
    void annotationTest() {
        AnnotationExClass annotationExClass = new AnnotationExClass();
        System.out.println("defaultName : " + annotationExClass.getDefaultName() + "\ncustomName : " + annotationExClass.getName2());


        Field[] fields = annotationExClass.getClass().getDeclaredFields();
        for(Field field : fields){
            System.out.print(field.getName() +" : ");
            Annotation[] annotations = field.getDeclaredAnnotations();
            for (Annotation annotation : annotations) {
                CustomAnnotation customAnnotation = (CustomAnnotation) annotation;
                System.out.println(customAnnotation.name());
            }
        }
    }

결과물을 보면 매개변수의 값은 이름없음이지만 각 필드에 할당된 어노테이션의 필드 값은 다른 것을 볼 수 있다.

result

만일, 어노테이션에 RetentionRUNTIME으로 하지 않으면 아래와 같이 런타임시에 동작하는 reflection을 이용해서 값을 불러오지 못한다.

fail



애노테이션 프로세서

런타임시에 리플랙션을 사용하는 어노테이션과는 달리 컴파일 타임에 이루어진다.

컴파일 타임에 어노테이션들을 프로세싱하는 javac에 속한 빌드 툴로 어노테이션의 소스코드를 분석하고 처리하기 위해 사용되는 훅이다.

보일러플레이트 코드를 제거하는데 도움이 된다.
( AbstractProcessor를 implements하여 구현체를 만들 수 있으며 Lombok의 @Getter, @Setter와 같은 어노테이션을 이용하는 것만으로도 컴파일 타임에 알아서 getter/setter를 만들어주는 방식으로 보일러플레이트 코드 제거 )





Reference

https://hiddenviewer.tistory.com/88

https://stackoverflow.com/questions/2146104/what-is-annotation-processing-in-java

http://hannesdorfmann.com/annotation-processing/annotationprocessing101/

https://medium.com/@jason_kim/annotation-processing-101-%EB%B2%88%EC%97%AD-be333c7b913