자바 애노테이션 깊게 살펴보기

Annotation

자주 사용하고 있지만, 잘 모를 수 있는 애노테이션에 관해 살펴보도록 하겠다.

자바 애노테이션은 JDK 1.5버전 부터 제공하는 기능으로, 자바 소스코드에 추가하여 사용할 수 있는 메타데이터의 일종이다.

잘 알고 있겠지만 스프링의 경우 이 애노테이션을 활용하여 다양한 스프링의 기능들을 사용할 수 있다. ( 빈 등록, 빈 검색 등등 )

자바 애노테이션의 경우 기본적으로 클래스 파일에 임베디드 되어 자바 컴파일러에 의해 생성된 후 JVM에 포함되어 작동한다.

알아두면 좋은 지식 - 내장 애노테이션 -

자바 언어에 내장된 애노테이션들은 다음과 같다.

  • @Override

  • @Deprecated

  • @SuppressWarnings

애노테이션에 사용되는 메타 애노테이션은 다음과 같다.

오늘 우리가 살펴볼 애노테이션들이기도 하다.

  • @Retention

  • @Documented

  • @Target

  • @Inherited

그리고 자바 7부터는 다음과 같은 애노테이션이 언어에 추가되었다.

  • @SafeVarargs

  • @FunctionalInterface

  • @Repeatable

자바 애노테이션 살펴보기

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface CustomAnnotation {
}

위에 언급했듯이 자바의 애노테이션은 위와 같이 4개의 메타애노테이션을 가질 수 있다.

지금부터 하나씩 자세히 살펴보도록 하겠다.

@Retention

@Retention 애노테이션의 RetentionPolicy는 애노테이션을 언제 까지 유지할 지에 대한 정책이다.

여기서 말하는 언제란,

  • 런타임까지 유지시킬 것인지

  • 컴파일 타임에만 유지시킬 것인지
  • 그것도 아니면 애노테이션의 원래 용도인 주석으로써의 역할만 가져가게 할 것인지

에 대한 정책이다.

/**
 * Annotation retention policy.  The constants of this enumerated type
 * describe the various policies for retaining annotations.  They are used
 * in conjunction with the {@link Retention} meta-annotation type to specify
 * how long annotations are to be retained.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

RetentionPolicy 내부를 열어보면 다음과 같이 3 가지 특성에 대해 명시하고 있다.

각각의 설명을 해석해보자면,

SOURCE

컴파일러에 의해 제거되는 Retension 정책이다. 주석으로써의 애노테이션을 사용할 때 사용한다.

CLASS

컴파일러에 의해 애노테이션을 클래스 파일에 저장하지만, 런타임 시에는 사라지게 된다.

이 말은 즉, 런타임에 리플렉션으로 해당 애노테이션의 정보를 가져올 수 없다는 것을 의미한다.

따로 애노테이션의 Retension 정책을 가져가지 않으면 디폴트 값이다.

RUNTIME

컴파일러에 의해 클래스파일에 저장되어지며, 애노테이션을 런타임에도 사용할 수 있는 정책이다.

런타임에 리플렉션을 사용해 애노테이션의 정보를 읽어오는 것이 가능하다.

눈으로 확인해보자

백문이 불여일견

지금부터 해당 애노테이션이 어떻게 클래스파일로 바뀌는지 눈으로 확인해보자.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface CustomAnnotation {
}
public class Harry {
    @CustomAnnotation
    public void sayMyName() {
        System.out.println("My Name is Harry");
    }
}

Harry라는 클래스의 sayMyName에 위에서 만들어둔 @CustomAnnotation 애노테이션을 메소드에 사용하였다.

RetentionPolicy은 RUNTIME이다.

그리고 나서 아래는 class 파일로 컴파일된 클래스 파일이다.

// Harry.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class Harry {
    public Harry() {
    }

    @CustomAnnotation
    public void sayMyName() {
        System.out.println("My Name is Harry");
    }
}

당연히 클래스 파일에도 애노테이션이 존재하는 것을 확인할 수 있다.

CLASS 정책도 확인해보자.

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface CustomAnnotation {
}
// Harry.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class Harry {
    public Harry() {
    }

    @CustomAnnotation
    public void sayMyName() {
        System.out.println("My Name is Harry");
    }
}

CLASS 역시 .class 파일에는 남아있는 것을 확인할 수 있다.

여기까지 보면 어라 둘 다 남아있으면 CLASS 역시 런타임에 리플렉션으로 가져올 수 있는 것 아닌가 라고 생각할 수 있다.

이는 직접 .class 파일의 bytecode를 보면 확인할 수 있다.

아래는 Retension CLASS 정책의 바이트 코드이다.

// class version 58.0 (58)
// access flags 0x21
public class Harry {

  // compiled from: Harry.java

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

  // access flags 0x1
  public sayMyName()V
  @LCustomAnnotation;() // invisible
   L0
    LINENUMBER 4 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "My Name is Harry"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 5 L1
    RETURN
   L2
    LOCALVARIABLE this LHarry; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}
// class version 58.0 (58)
// access flags 0x21
public class Harry {

  // compiled from: Harry.java

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

  // access flags 0x1
  public sayMyName()V
  @LCustomAnnotation;()
   L0
    LINENUMBER 4 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "My Name is Harry"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 5 L1
    RETURN
   L2
    LOCALVARIABLE this LHarry; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

쉽게 비교하기 위해 애노테이션만 따로 추려보자면,

// Retension이 Runtime
  public sayMyName()V
  @LCustomAnnotation;()
   L0
    LINENUMBER 4 L0

// Retension이 Class
  public sayMyName()V
  @LCustomAnnotation;() // invisible
   L0
    LINENUMBER 4 L0

바이트 코드 변환을 하는 여러 도구들이 존재하는데, 나는 여기서 Intellij에서 제공하는 도구를 사용하였다.

그래서 그런지 // invisible이라는 주석만으로 RUNTIME과 CLASS를 구분하는게 마음에 안들어서 조금 더 리서치를 해보았다.

그러다 다음과 같은 문서를 찾았다.

https://www.javassist.org/html/javassist/bytecode/AnnotationsAttribute.html

이 문서에 따르면 바이트 코드에서 사용하는 AnnotationsAttribute 클래스가 존재한다는 사실을 알게 되었다.

스크린샷 2020-09-30 오후 6 45 53

이 클래스에는 두 개의 필드가 존재하는데 RuntimeVisibleAnnotation과 RuntimeInVisibleAnnotation이다.

이 두 필드의 값을 통해 해당 애노테이션을 런타임까지 유지할지, 버릴지를 결정하는 정책을 이 필드로 판단한다는 사실을 알게 되었다.

바이트 코드에서 @LCustomAnnotation로 보여지지만 내부적으로는 AnnotationsAttribute의 태그 값이 Visible이냐, Invisible이냐에 따라 구분하는 것으로 이해하였다.

@Documented

  • 친구랑 약속이 있어서 이 부분은 나중에 작성 예정 -

Reference

https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94_%EC%95%A0%EB%84%88%ED%85%8C%EC%9D%B4%EC%85%98

https://www.javassist.org/html/javassist/bytecode/AnnotationsAttribute.html



© 2022. by minkuk

Powered by minkuk