후벼파는 자바 - 어노테이션의 내부 원리
자바를 어느정도 해보신 분들은 알겠지만 @를 붙여서 사용하는 어노테이션을 써보았을 것이다.
어노테이션이 어떤식으로 동작하는지 궁금해서 몇 가지 테스트를 해보았다.
실습을 했던 코드는 Ayoub El Abbassi 님의 블로그의 "How to add Annotations at Runtime to a java class method using Javassist?"글의 코드를 참고로 하였다.
우선 코드를 만들고
package annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface PersonneName {
public String name();
}
컴파일을 한 후에 바이트코드를 확인해 보았다.
> javac annotation.PersonneName.java
> javap -c annotation.PersonneName
Compiled from "PersonneName.java"
public interface annotation.PersonneName extends java.lang.annotation.Annotation{
public abstract java.lang.String name();
}
1. java.lang.annotation.Annotation을 상속
@interface의 타입은 java.lang.annotation.Annotation을 상속을 받는다는 것을 알 수가 있다.
public interface annotation.PersonneName extends java.lang.annotation.Annotation
Annotation 인터페이스에 대한 JavaDoc API를(링크) 가보면 The Java™ Language Specification의 9.6에 자세히 나와 있다고 한다.
책을 찾아보니, (p.271)
'어노테이션 타입(annotation type)' 선언은 특별한 종류의 인터페이스이다. 어노테이션 타입 선언을 일반적인 인터페이스 선언과 구분하려면 예약어 interface 앞에 기호 @을 붙인다.
참고. 기호 @과 예약어 interface는 별개의 토큰이다. 기술적으로 이 둘은 공백으로 분리가 가능하나, 스타일의 문제상 분리하지 않는 것을 권장한다.
참고. 어노테이션 타입 선언은 문맥 자유 구문(Context-free grammar, CFG)으로부터 다음과 같은 제한을 갖는다.
- 어노테이션 타입 선언은 제네릭일 수 없다.
- extends 절을 가질 수 없다 (어노테이션 타입은 암묵적으로 java.lang.annotation.Annotation을 확장한다.)
- 메소드는 매개변수를 가질 수 없다.
- 메소드는 타입 매개변수를 가질 수 없다.
- 메소드 선언은 throws 절을 가질 수 없다.
위에 참고에 보면 @interface는 상속을 받을 수 없다고 한다. 이유는 내부적으로 Annotation 인터페이스를 상속을 받으므로...
2. 메소드는 추상 메소드
사실 어노테이션의 특징이 아닌 일반적인 인터페이스의 특징인데 이번 기회에 알게 되었다. 어노테이션도 인터페이스의 한 종류이다. (자바 스펙에서는 같은 같은 인터페이스 장에서 다룬다)
public abstract java.lang.String name();
3. 런타임시 어노테이션 정보 획득
사용하는 측 코드는 아래와 같다.
package annotation;
import java.lang.reflect.Method;
public class SayHelloBean {
private static final String HELLO_MSG = "Hello ";
@PersonneName(name = "World !! (simple annotation)")
public String sayHelloTo(String name) {
return HELLO_MSG + name;
}
public static void main(String[] args) {
try {
SayHelloBean simpleBean = new SayHelloBean();
Method helloMessageMethod = simpleBean.getClass()
.getDeclaredMethod("sayHelloTo", String.class);
PersonneName mySimpleAnnotation = (PersonneName) helloMessageMethod
.getAnnotation(PersonneName.class);
System.out.println(simpleBean.sayHelloTo(mySimpleAnnotation.name()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
예상했겠지만 어노테이션 정보는 리플렉션을 통해 구할 수 있다.
여기 예제에서는 메소드를 대상으로 하는 어노테이션을 사용하였기에 Method 인스턴스를 획득을 하여서 getAnnotation메소드를 호출하여 어노테이션 정보를 구했다.
@Target(value = ElementType.METHOD)
1. SayHelloBean 객체 생성
2. getClass()를 통해(SayHelloBean는 java.lang.Object를 암묵적으로 상속) java.lang.Class 인스턴스 획득
3. java.lang.Class.getDeclaredMethod("sayHelloTo", String.class) 를 통해 sayHelloTo 이름과 파라메터로 String 타입을 가지는 java.lang.reflect.Method 인스턴스 획득
4. java.lang.reflect.Method.getAnnotation(PersonneName.class) 을 통해 해당 메소드에 붙어있는 PersonneName 타입의 어노테이션을 획득
5. PersonneName 타입의 name() 메소드를 호출하여 SayHelloBean.sayHelloTo 메소드에 붙였던 name 값을 획득
4. RetentionPolicy이 다른 타입일 때는?
java.lang.annotation.RetentionPolicy의 이늄(enum)은 세 가지 종류가 있다.
SOURCE, CLASS, RUNTIME
RetentionPolicy.SOURCE 이나 RetentionPolicy.CLASS로 바꾸어서 실행해보면 NullPointerException이 발생한다.
java.lang.NullPointerException
at annotation.SayHelloBean.main(SayHelloBean.java:27)
왜냐하면 이 둘로 선언되면 컴파일 될 때때 어노테이션이 폐기가 되기 때문이다.
CLASS는 폐기가 되지만 컴파일 될 때 클래스 파일에 기록이 되지만, 런타임시까지 남아있지 않는다.
또한 @Retention를 생략시 기본 값은 RetentionPolicy.CLASS이다.
RetentionPolicy.SOURCE인 예) @Override
RetentionPolicy.RUNTIME인 예) @Inject (javax.inject) , 스프링의 @Controller, @RequestMapping
5. 스프링에서는?
스프링에서는 어노테이션을 많이 사용한다.
코드 분석을 깊게 하지 않아서 자세한 내용은 생략... 하면 그렇고
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
부분을 보면 찾을 수 있을 것 같다.
세부적인 리플렉션 관련 동작은 유틸리티 클래스인 org.springframework.util.ReflectionUtils에서 하는 것으로 보였다.