자바 애노테이션 깊게 살펴보기
in Programming Language on Java
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 클래스가 존재한다는 사실을 알게 되었다.
이 클래스에는 두 개의 필드가 존재하는데 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