본문 바로가기

Programing/JVM(Java, Kotlin)

후벼파는 자바 - 어노테이션의 내부 원리

자바를 어느정도 해보신 분들은 알겠지만 @를 붙여서 사용하는 어노테이션을 써보았을 것이다.

어노테이션이 어떤식으로 동작하는지 궁금해서 몇 가지 테스트를 해보았다.


실습을 했던 코드는 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에서 하는 것으로 보였다.