본문 바로가기

Java/Java SE, EE

[Java/Java SE, EE] 어노테이션(Annotation)과 적용 대상(@Target), 유지 정책(@Retention)

어노테이션(Annotation)

자바의 어노테이션은 일종의 메타데이터(Metadata)입니다. 메타데이터란 어떤 데이터에 대한 구조화된 데이터로써, 간단히 말해 다른 데이터를 설명하기 위한 데이터입니다.

자바에서 어노테이션은 컴파일 또는 실행 과정에서 어떤 데이터를 어떻게 처리할 것인지에 대한 표현 목적으로 사용합니다. 어노테이션이 표현하는 데이터의 유형은 클래스, 필드 또는 메소드 등이 될 수 있습니다. 아래 코드는 어떤 클래스에 대한 메타데이터 표현을 위한 어노테이션을 사용하고 있는 예시입니다. @SomeAnno 어노테이션은 SomeClass 클래스의 메타데이터를 표시합니다.

@SomeAnno
public class SomeClass {
   
}

어노테이션을 정의하는 방법은 다음과 같습니다. 이는 클래스 또는 인터페이스를 정의하는 방법과 매우 유사합니다.

[접근 제한자 public|protected|default|private] @interface [어노테이션 이름] {
	
}

어노테이션은 엘리먼트(Element)라고 불리우는 멤버를 가질 수 있습니다. 클래스에서의 필드와 비슷한 역할을 합니다.

[접근 제한자 public|protected|default|private] @interface [어노테이션 이름] {
	[데이터 타입] [엘리먼트 이름]() [디폴트 값] 
}

엘리먼트의 디폴트 값은 생략 될 수 있으며, 디폴트 값이 생략된 엘리먼트는 어노테이션을 사용할 시 반드시 엘리먼트의 값을 입력하도록 컴파일러가 강제합니다.

적용 대상(@Target)

어노테이션은 클래스, 필드 또는 메소드 등에 적용 될 수 있습니다. 어노테이션의 적용 대상은 java.lang.annotation.ElementType 열거 상수에 미리 정의되어 있습니다.

java.lang.annotation.ElementType 적용 범위
TYPE 클래스, 인터페이스, 열거 타입
ANNOTATION_TYPE 다른 어노테이션
CONSTRUCTOR 생성자
FIELD 필드
METHOD 메소드
LOCAL_VARIABLE 로컬 변수
PACKAGE 패키지

어노테이션의 적용 대상을 지정하기 위해서는 @Target 어노테이션을 사용합니다. @Target은 다른 어노테이션의 메타데이터를 정의하는 어노테이션입니다. 이처럼 어노테이션의 메타데이터를 정의하는 어노테이션은 ANNOTATION_TYPE 적용 대상으로 지정됩니다.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

예를 들어, 어떤 필드에 대해서만 사용하고자 하는 어노테이션은 다음과 같이 정의 될 수 있습니다.

@Target(ElementType.FIELD)
public @interface SomeAnno {

}

적용 대상을 하나 이상 지정하고 싶다면 중괄호({})를 사용하여 java.lang.annotation.ElementType를 열거합니다.

@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface SomeAnno {

}

유지 정책(@Retention)

어노테이션은 유지 정책에 따라서 지속 가능한 범위를 정의합니다. java.lang.annotation.RetentionPolicy 열거 상수는 어노테이션이 지속 가능한 범위를 미리 정의하고 있습니다. 지속 가능한 범위는 SOURCE, CLASS, RUNTIME 순서로 확장됩니다.

java.lang.annotation.RetentionPolicy 지속 가능 범위
SOURCE 어노테이션이 소스 파일에서만 사용됩니다. 소스 파일을 분석하는데 도움을 줄 수 있으나, 바이트 코드 파일에는 포함되지 않습니다
CLASS 어노테이션이 소스 파일 ~ 바이트 코드 파일까지 사용됩니다. 런타임 단계까지는 유지되지 않으므로 리플렉션에 의해 탐색되지 않습니다
RUNTIME 어노테이션이 소스 파일 ~ 런타임 단계까지 사용됩니다. 모든 범위에서 사용될 수 있으며, 런타임 단계에서 리플렉션에 의해 탐색 될 수 있습니다

대표적인 SOURCE 유지 정책의 사용은 @Override 어노테이션이 있습니다. @Override는 부모 클래스가 갖는 메소드를 상속 관계에 의해 재정의 했을 때, 컴파일러가 문법 검사의 대상으로 삼도록 합니다. 컴파일러는 문법에 오류가 있는 경우 컴파일을 중단하며, 문법의 수정을 요청합니다. 따라서 SOURCE 유지 정책은 컴파일 단계에서는 필요하지만, 컴파일이 완료된 이후에는 더 이상 사용되지 않는 어노테이션에 적용됩니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

 

CLASS와 RUNTIME 유지 정책의 차이는 런타임 단계에서의 지속 가능 여부입니다. 이는 리플렉션에서의 사용 가능 여부와 직결되기 때문입니다.

리플렉션(Reflection)을 사용하면, 어떤 데이터에 대한 클래스, 필드, 메소드, 어노테이션 등에 대한 메타데이터를 런타임 단계에서 탐색 할 수 있습니다. 예를 들면, 애플리케이션이 실행 중에 어떤 데이터에 대한 어노테이션을 읽어와서, 해당 데이터가 어떤 메타데이터로 표시되고 있는지에 따라서 비즈니스 로직이 분기 처리 될 수 있습니다. 자세한 내용은 이어서 소개하도록 하겠습니다.

우선 유지 정책을 지정하기 위해서는 @Retention 어노테이션을 사용합니다. @Retention 역시 @Target처럼 다른 어노테이션의 메타데이터를 정의하는 어노테이션입니다

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

RetentionPolicy는 SOURCE, CLASS, RUNTIME 중 하나를 지정하며, 여러 개의 유지 정책을 동시에 적용 할 수는 없습니다.

@Retention(RetentionPolicy.RUNTIME)
public @interface SomeAnno {

}

리플렉션을 통한 실사용 예시

이번에는 어노테이션을 런타임 중에 탐색하고, 어노테이션의 엘리먼트에 따라서 로직을 처리하는 예시를 작성해보겠습니다. 우선 클래스에 적용하기 위한 어노테이션을 다음과 같이 정의합니다. 이 어노테이션은 클래스에 대한 설명(description)을 표시합니다.

@Target( ElementType.TYPE )
@Retention(RetentionPolicy.RUNTIME)
public @interface SomeAnnoForClass {
   String description();
}

다음으로 필드에 적용하기 위한 어노테이션을 정의합니다. 이 어노테이션은 필드가 0(zero) 값을 가질 수 있는지에 대한 속성을 표시합니다.

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SomeAnnoForField {
   boolean can_not_be_zero();
}

다음으로 클래스를 하나 생성하여 앞서 정의한 어노테이션을 적용하도록 하겠습니다. @SomeAnnoForClass 어노테이션은 작성한 클래스에 대한 설명을 표시하고 있으며, 필드 a와 b는 @SomeAnnoForField 어노테이션에 의해서 각각 0(zero) 값의 저장 가능 여부를 서로 다르게 표시합니다(필드 a는 0을 저장 할 수 없습니다).

@SomeAnnoForClass(description = "This sample code show usages of annotation")
public class SomeClass {
   @SomeAnnoForField(can_not_be_zero = true)
   public int a;
   @SomeAnnoForField(can_not_be_zero = false)
   public int b;
   
   public SomeClass(int _a, int _b) {
      a = _a;
      b = _b;
   }
}

물론, 이러한 어노테이션의 표시 성질은 우리가 프로그램에서 임의로 결정한 것으로 실제 동작과는 무관합니다. 이러한 성질들이 원하는대로 동작하기 위해서는 직접 어노테이션에 대한 처리 코드를 작성해야만 합니다. 예를 들어, 아래 코드는 validate() 메소드에서 클래스에 지정한 각 어노테이션을 탐색하여 사용하는 방법을 보여줍니다.

@SomeAnnoForClass(description = "This sample code show usages of annotation")
public class SomeClass {
   @SomeAnnoForField(can_not_be_zero = true)
   public int a;
   @SomeAnnoForField(can_not_be_zero = false)
   public int b;
   
   public SomeClass(int _a, int _b) {
      a = _a;
      b = _b;
   }
   
   public void validate() throws IllegalAccessException {
      Class<? extends SomeClass> clazz = this.getClass();
      
      SomeAnnoForClass anno_for_class = clazz.getAnnotation(SomeAnnoForClass.class);
      if (null != anno_for_class) {
         System.out.println(anno_for_class.description());
      }
      
      Field[] fields = clazz.getFields();
      for (Field field : fields) {
         SomeAnnoForField anno_for_field = field.getAnnotation(SomeAnnoForField.class);
         if (null != anno_for_field) {
            if (anno_for_field.can_not_be_zero()) {
               Class<?> field_clazz = field.getType();
               Object field_value = field.get(this);
               
               if (field_clazz.equals(int.class)) {
                  if ((int) field_value == 0) {
                     System.out.println(field.getName() + " field could not be zero value");
                  }
               }
            }
         }
      }
   }
}

어노테이션을 획득하려면 기본적으로 클래스(class) 타입 획득해야 합니다. 이 정보는 인스턴스의 getClass() 메소드를 사용하여 획득 할 수 있습니다. class 타입을 알게 되면 해당 class 타입에 지정된 어노테이션을 getAnnotation() 메소드를 사용하여 획득 할 수 있습니다. 이때 탐색하려는 어노테이션의 타입을 지정해야 하며, 지정된 어노테이션을 탐색하였으나 찾지 못한 경우 null을 리턴합니다. 일단 어노테이션에 대한 정보를 획득하면, 해당 어노테이션이 표시하는 엘리먼트에 대해서 접근이 가능합니다.

만약 필드에 대한 어노테이션에 접근하고자 한다면, 우선 클래스(class) 타입을 먼저 획득하고 클래스 타입으로부터 필드의 정보를 얻기 위해 getFields()를 사용합니다.  각 필드는 클래스와 마찬가지로 getAnnotation() 메소드를 사용하여 어노테이션 정보를 탐색 할 수 있습니다.

public static void main(String[] _args) throws IllegalAccessException {
   SomeClass a = new SomeClass(0, 0);
   a.validate();
}

예시에서 validate()의 실행 결과는 다음과 같습니다.

This sample code show usages of annotation
a field could not be zero value