본문 바로가기
자바

[자바] Annotations, 자바에서 메타 데이터를 제공하는 방법

by 책 읽는 개발자_테드 2021. 8. 29.
반응형

학습할 것

· 애노테이션이란?

· 자바에서 사용하기 위해 미리 정해져 있는 어노테이션

     - @Override

     - @Deprecated 

     - @Supress Warnings

· 애노테이션 정의하는 방법

· 어노테이션을 선언하기 위한 메타 어노테이션

    - @Target, 애노테이션 적용 대상 설정하기

    - @Retention, 애노테이션 유지 정책

    - @Documented

    - @Inherited

- 애노테이션 프로세서


애노테이션이란?

· 자바 소스 코드에 '@'예약어를 추가하여 사용하는 메타데이터

 메타데이터: 애플리케이션이 처리해야 할 데이터가 아닌, 컴파일 과정과 실행 과정에서 코드를 어떻게 컴파일하고 처리할 것인지 알려주는 정보

 

· enum 클래스와 같이 어노테이션도 상속이 불가능하다. 즉, extends 예약어를 사용할 수 없다.

     - 미리 만들어 놓은 어노테이션을 확장 할 수 없다는 뜻 

· 설정이 필요한 위치에 관련 설정이 존재는 특성으로 코드에 대한 가독성을 높인다.

     - 어노테이션이 만들어지기 전에는 자바 애플리케이션의 설정을 위해 XML, properties 파일을 따로 지정하는 방식만 존재했다.

애노테이션의 용도 

1. 컴파일러에게 코드 문법 에러를 체크하도록 정보 제공

ex) @Override 어노테이션은 메소드가 오버라이드된 것임을 컴파일러에게 알려주어 컴파일러가 오버라이드 검사를 하도록 한다. 오버라이드가 되지 않았다면 컴파일러는 에러를 발생한다.

 

2. 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보 제공

ex) 자바 프레임워크 스프링은 XML 설정 파일을 대체하여 애노테이션을 통해 코드를 자동 생성 할 수 있다.

 

3. 런타임 시 특정 기능을 실행하도록 정보 제공

ex) Junit에서 리플렉션을 통해 런타임의 @Test로 표시된 메서드가 단위 테스트임을 알 수 있다.

 

자바에서 사용하기 위해 미리 정해져 있는 애노테이션

@Override

· 해당 메소드가 부모 클래스에 있는 메소드를 Override 했다는 것을 명시적으로 선언

· 선언한 메소드가 부모 클래스에 없다면, 컴파일 오류가 발생

 

@Deprecated 

· 클래스가 메소드가 더 이상 사용되지 않는 경우 선언

· 해당 애노테이션을 선언한 클래스 또는 메소드를 사용하면 경고 발생

 

@Supress Warnings

· 컴파일러에서 알리는 경고를 제거


애노테이션을 정의하기

 @interface를 사용해서 어노테이션을 정의하고, 그 뒤에 사용할 이름을 붙인다.

public @interface AnnotationName {

}

 

정의한 애노테이션은 다음과 같이 사용한다.

@AnnotationName

 

애노테이션은 엘리먼트를 멤버로 가질 수 있다.

· 각 엘리먼트는 타입이름으로 구성, 디폴트 값을 가질 수 있음

·  엘리먼트 타입으로 기본 데이터 타입, 참조 타입(열거 타입, Class 타입, 배열 등) 모두 사용 가능

public @interface AnnotationName {

   String elementName1();
   int elementName2() default 5;

}
public @interface AnnotationName {

   String elementName1();
   int elementName2() default 5;

}

 

정의한 애노테이션 코드에 적용하기 (elementName2는 디폴트 값이 있기 때문에 생략 가능)

@AnnotationName(elementName1 = "값", elementName2 = 3);

OR

@AnnotationName(elementName1 = "값");

 

애노테이션은 기본 엘리먼트value를 가질 수 있다. 

public @interface AnnotationName {

   String value();
   int elementName2() default 5;

}

 

value 엘리먼트를 가진 애노테이션을 코드에 적용하면, 값만 기술해도 기본 엘리먼트인 value 값으로 자동 설정된다.

@AnnotationName(value = "값");

OR

@AnnotationName("값");

 

어노테이션을 선언하기 위한 메타 어노테이션

@Target, 애노테이션 적용 대상 설정하기 

· 애노테이션이 적용될 대상을 지정하려면 @Target 애노테이션을 사용

· @Target의 기본 엘리먼트인 value는 애노테이션이 적용될 대상을 복수로 지정하기 위해 ElementType 배열을 값으로 가짐

· java.lang.annotation.ElementType에 열겨 상수로 정의된 애노테이션을 적용할 수 있는 대상

ElementType 열거 상수 적용 대상
TYPE 클래스, 인터페이스, 열거 타입
ANNOTATION_TYPE 애노테이션
FIELD 필드
CONSTRUCTOR 생성자
METHOD 메소드
LOCAL_VARIABLE 로컬 변수
PACKAGE 패키지

 

예시

@Target({ElemenType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationName {

}

생성자는 @Target으로 지정되지 않아서 애노테이션을 적용할 수 없다.

@AnnotationName
public class ClassName {

   private String fieldName;
     
   // 애노테이션 적용 불가
   public ClassName(){}

   @AnnotationName
   public void methodName(){}
}

 

@Retention, 애노테이션 유지 정책

· 애노테이션 정의 시 사용 용도에 따라 @AnnotationName을 어느 범위까지 유지할 것인지 지정 가능

ex)소스상에만 유지, 컴파일된 클래스(바이트코드에 invisible이라는 주석과 함께 표시)까지 유지, 런타임(바이트코드에 표시)시에도 유지

상세:b-programmer.tistory.com/264

 

· java.lang.annotation.RetentionPolicy에 열거 상수로 정의된 애노테이션 유지 정책

RetentionPolicy 열거 상수 설명
SOURCE 소스상에서만 어노테이션 정보를 유지. 소스 코드를 분석할 때만 의미가 있고, 바이트 코드 파일에는 정보가 남지 않음.
CLASS 바이트 코드 파일까지 어노테이션 정보 유지. 하지만 리플렉션을 이용해 어노테이션 정보를 얻을 수는 없음.
RUNTIME 바이트 코드 파일까지 어노테이션 정보를 유지하면서 리플렉션을 이용해서 런타임시에 어노테이션 정보를 얻을 수 있음.

 

리플렉션으로 클래스 메타 정보 얻기

·   어노테이션 유지 정책을 지정할 때는 @Retention 어노테이션을 사용

·  리플렉션: 런타임 시에 클래스의 메타 정보를 얻는 방법

ex) 클래스가 가지고 있는 필드, 생성자, 메소드, 적용된 어노테이션이 무엇인지 알아내기

 

· 리플렉션을 이용해서 런타임 시에 어노테이션 정보를 얻으려면 어노테이션 유지 정책을 RUNTIME으로 설정 필요

·  @Retention의 기본 엘리먼트인 value는 RetentionPolicy 타입

 

예시 - 어노테이션 유지 정책 Runtime으로 설정

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationName {

}

 

런타임 시 어노테이션 정보 사용하기

·  어노테이션 자체는 아무런 동작을 가지지 않는 표식일 뿐이지만, 리플렉션을 이용해서 어노테이션의 적용 여부와 엘리먼트 값을 읽고 적절히 처리 가능

 

· 클래스에 적용된 어노테이션 정보를 얻으려면 java.lang.Class 클래스를 이용

· 필드, 생성자, 메소드에 적용된 어노테이션 정보를 얻으려면 Class 클래스의 아래 메소드들을 통해서 java.lang.reflect 패키지의 Field, Constructor, Method 타입의 배열을 얻어야한다.

리턴 타입 메소드명(매개 변수) 설명
Field[] getFields() 필드 정보를 Field 배열로 리턴
Constructor[] getConstructors() 생성자 정보를 Constructor 배열로 리턴
Method[] getDeclaredMethods() 메소드 정보를 Method 배열로 리턴

 

그런 다음 Class, Field, Constructor, Method가 가지고 있는 다음 메소드등르 호출해서 적용된 어노테이션 정보를 얻을 수 있다.

리턴 타입 메소드명(매개변수)
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
· 지정한 어노테이션의 적용 여부 리턴
· Class에서 호출했을 때 상위 클래스에 적용된 경우에도 true를 리턴
Annotation getAnnotation(Class<T> annotationClass)
· 지정한 어노테이션이 적용되어 있으면 어노테이션을 리턴하고 그렇지 않다면 null을 리턴
· Class에서 호출했을 때 상위 클래스에 적용된 경우에도 어노테이션을 리턴.
Annotation[] getAnnotations()
· 적용된 모든 어노테이션 리턴
· Class에서 호출했을 때 상위 클래스에 적용된 경우에도 어노테이션도 모두 포함
· 적용된 어노테이션이 없을경우 길이가 0인 배열을 리턴
Annotation[] getDeclaredAnnotations()
· 직접 적용된 모든 어노테이션을 리턴.
· Class에서 호출했을 때 상위 클래스에 적용된 어노테이션은 미포함

 

예시 - 어노테이션과 리플렉션을 이용해서 각 메소드의 실행 내용을 구분선으로 분리해서 콘솔에 출력하는 예제

PrintAnnotation의 기본 엘리먼트 value는 구분선에 사용될 문자, number는 반복 출력 횟수다. 

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation {

   String value() default "-";
   int number() default 15;
}

public class Service {

   @PrintAnnotation
   public void method1() {
       System.out.println("실행 내용1\n");
   }

   @PrintAnnotation("*")
   public void method2() {
       System.out.println("실행 내용2\n");
   }

   @PrintAnnotation(value="#", number=20)
   public void method3() {
       System.out.println("실행 내용3\n");
   }
}

 

public class Main {

   public static void main(String [] args){

       //Service 클래스에서 메소드 정보 얻기
       Method[] declaredMethod = Service.class.getDeclaredMethods(); //Service 클래스에 선언된 메소드 얻기(리플렉션)

       //Method 객체 하나씩 처리
       for(Method method: declaredMethod){
           //PrintAnnotation이 적용되었는지 확인
           if(method.isAnnotationPresent(PrintAnnotation.class)){
               //PrintAnnotation 객체 얻기
               PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);

               //메소드 이름 출력
               System.out.println("[" + method.getName() + "]");

               //구분선 출력
               for (int i =0; i<printAnnotation.number(); i++){
                   System.out.print(printAnnotation.value());
               }

               System.out.println();

               try{
                   //메소드 호출
                   method.invoke(new Service());
               }catch (Exception e){
                   System.out.println();
               }
           }
       }
   }
}

 

@Documented 

· 애너테이션에 대한 정보를 javadoc으로 작성한 문서에 포함

·  자바에서 제공하는 기본 애너테이션 중에 ‘@Override’와 ‘@SuppressWarnings’를 제외하고 모두 이 메타 애너테이션이 붙어 있다.

 

예시 - @Retention 애너테이션

@Documented
@Retention(value=RUNTIME)
@Target(value=ANNOTATION_TYPE)
public @interface Retention

 

애노테이션 프로세서

애노테이션 프로세싱은 컴파일 타임에 어노테이션을 분석한다. 그리고 붙여진 어노테이션 정보를 참고하여 다음과 같은 작업을 한다.

 

1. 애노테이션 클래스 생성

2. 애노테이션 파서 클래스 생성

3. 애노테이션 사용

4. 컴파일 후 애노테이션 파서로 애노테이션 다루기

5. 자동 생성된 클래스를 빌드 폴더에 더하기

 

이러한 애노테이션 파서를 구현하기 위해 자바에서는 AbstractProcessor 추상 클래스를 제공하여, 이것을 상속 받은 클래스는구현을 위한 다양한 메소드들을 재정의 할  수 있다.

 

예시 - AbstractProcessor를 상속받은 클래스

public class SCAbstractProcessor extends AbstractProcessor {

   @Override
   public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
       return false;
   }

   @Override
   public Set<String> getSupportedAnnotationTypes() { ... }

   @Override
   public SourceVersion getSupportedSourceVersion() { ... }

}

 

출처

이것이 자바다

자바의 신

백기선님 온라인 자바스터디

f-lab

반응형

댓글