12회차. 애노테이션

KIMA·2023년 2월 12일
0
post-thumbnail

목표

자바의 애노테이션에 대해 학습한다.

학습할 것

애노테이션(annotation)이란?

애노테이션은 주석이라는 뜻으로, 프로그램의 소스코드안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이다.
주석처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공한다.

예를들어, 자신이 작성한 소스코드 중에서 특정 메소드만 테스트하길 원한다면, 다음과 같이 @Test라는 애노테이션을 메서드 앞에 붙여 테스트 프로그램에게 해당 메소드가 테스트 대상임을 알린다.

@Test
public void method() {}

애노테이션 타입 정의하기

@Override는 애노테이션이고, Override는 애노테이션의 타입이다.

애노테이션 타입을 정의하는 방법은 다음과 같다.

@interface 애노테이션명 {
  타입 요소명();
}
  • 인터페이스처럼 상수를 정의할 수 있지만, 디폴트 메소드는 정의할 수 없다.
  • 애노테이션은 상속이 불가능하다.

애노테이션의 요소

애노테이션 내에 선언된 메소드를 애노테이션의 요소라고 한다.

애노테이션의 요소는 반환값이 있고 매개변수는 없는 추상 메소드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다.
다만, 애노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다.

  • 요소의 타입은 기본형, String, enum, 애노테이션, Class만 허용된다.

  • 요소에 예외를 선언할 수 없다.

  • 요소를 타입 매개변수로 정의할 수 없다.

  • 요소의 이름도 같이 적어주므로 순서는 상관없다.

    • 요소 개수가 오직 하나뿐이고 이름이 value인 경우, 이름을 생략하고 값만 적어도 된다.
  • 요소 타입이 배열일 경우, 괄호{}를 사용해서 여러 개의 값을 지정할 수 있다.

    • 타입이 배열이지만 값이 하나일 경우, 괄호{}는 생략이 가능하다.
  • 요소가 하나도 정의되지 않은 애노테이션을 마커 애노테이션이라 한다.

    • 예) @Override, @Test
  • 예제

    @interface TestInfo {
      int count();
      String testedBy();
      String[] testTools();
      TestType testType(); // enum TestType { FIRST, FINAL }
      DateTime testDate(); // 자신이 아닌 다른 애노테이션(@Datetime)을 포함할 수 있다.
    }
    
    @interface DateTime {
      String yymmdd();
      String hhmmss();
    }
    @TestInfo(
      count = 3, testedBy = "Kim",
      testTools = { "JUnit", "AutoTester" },
      testType = TestType.FIRST,
      testDate = @DateTime(yymmdd="230212", hhmmss="202900")
    )
    public class MyClass {}
  • 애노테이션의 각 요소는 기본값을 가질 수 있다.

    • null을 제외한 모든 리터럴이 가능하다.
    @interface TestInfo {
      int count() default 1; // 기본값을 1로 지정
    }

java.lang.annotation.Annotation

모든 애노테이션의 조상이다.

다음은 Annotation의 구조이다.
Annotation의 구조

따라서, 모든 애노테이션 객체에 대해 equals(), hashcode(), toString(), annotationType() 호출이 가능하다.
다음은 Annotation 클래스에 적용된 모든 애노테이션에 대해 해당 메소드들을 호출한다.

Class<AnnotationTest> cls = AnnotationTest.class;
Annotation[] annoArr = cls.getAnnotations();

for(Annotation anno : annoArr) {
  System.out.println("toString(): " + anno.toString());
  System.out.println("hashcode(): " + anno.hashCode());
  System.out.println("equals(): " + anno.equals(anno));
  System.out.println("annotationType(): " + anno.annotationType());
}
toString(): @java.lang.Deprecated(forRemoval=false, since="")
hashcode(): 2011250702
equals(): true
annotationType(): interface java.lang.Deprecated

애노테이션의 종류

애노테이션은 크게 JDK에서 기본적으로 제공하는 것(= 표준 애노테이션), 메타 애노테이션, 사용자 정의 애노테이션으로 구성되어 있다.

사용자 정의 애노테이션은 위의 애노테이션 정의에서 살펴보았다.

표준 애노테이션

JDK에서 기본적으로 제공하는 애노테이션
주로 컴파일러에게 유용한 정보를 제공하기위해 사용한다.

  • java.lang.annotation 패키지에 포함되어 있다.

다음은 대표적인 표준 애노테이션들이다.

@Override

컴파일러에게 해당 메소드가 오버라이딩한 메소드라는 것을 알린다.

  • 해당 메서드가 조상 클래스에 있는지 확인하고, 없으면 컴파일 에러를 던진다.
    에러메시지를 출력

  • 오버라이딩할 때 메소드명을 잘못 적을 경우, 해당 사실을 알아채기 힘드므로 반드시 @Override를 붙여 실수를 미연에 방지하는 것이 좋다.

    class Parent {
      void parentMethod() {}
    }
    class Child extends Parent {
      void parentmethod() {} // 오버라이딩하려 했으나 실수로 메서드명을 잘못 적음
    }
    • 컴파일러는 그저 새로운 이름의 메서드가 추가된 것으로 인식하고, 오류를 발생하지 않는다.

@Deprecated

새로운 버전의 JDK가 소개될 때, 새로운 기능이 추가될 뿐만 아니라 기존의 부족했던 기능들을 개선하기도 한다. 이 과정에서 기존의 기능을 대체할 것들이 추가되어도, 이미 여러 곳에서 사용되고 있는 기존의 것들을 함부로 삭제할 수 없다.
따라서 더 이상 사용되지 않는 필드나 메서드에 @Deprecated를 붙여 해당 필드나 메서드는 다른 것으로 대체되었으니 더 이상 사용하지 않을 것을 권한다.

예를들어 java.util.Date클래스의 getDate()에는 @Deprecated가 붙어있는데,
getDate()의 설명을 보면 해당 메서드 대신에 JDK 1.1버전 부터 추가된 Calendar.get()을 사용하라고 되어있다.
deprecated getDate()

  • 만약 @Deprecated가 붙은 필드나 메소드를 사용할 경우, 컴파일할 때 아래와 같은 메시지가 나타난다.
    PS C:\...> javac AnnotationExample.java
    Note: AnnotationExample.java uses or overrides a deprecated API.
    Note: Recompile with -Xlint:deprecation for details.
    해당 소스파일이 deprecated된 대상을 사용하고 있으며, -Xlint:deprecation 옵션을 붙여 재컴파일하면 자세한 내용을 확인할 수 있다는 뜻이다.

@FunctionalInterface

함수형 인터페이스를 선언할 때, 이 애노테이션을 붙이면 컴파일러가 함수형 인터페이스를 올바르게 선언했는지 확인하고 잘못된 경우 에러를 발생시킨다.

예를들어 함수형 인터페이스를 선언할 때 함수형 인터페이스는 추상 메소드가 하나뿐이어야 한다 제약을 어기면 컴파일 에러가 발생한다.

  • 실수를 방지하기 위해 함수형 인터페이스를 선언할 때는 이 애노테이션을 붙이자.

@SuppressWarnings

컴파일러가 보여주는 경고메시지가 나타나지 않게 억제(Suppress)해준다.

컴파일러의 경고메시지는 모두 확인하고 해결해서 컴파일 후에 어떠한 메시지도 나타나지 않도록 해야하지만,
경고가 발생할 것을 알면서도 묵언해야할 때는 해당 애노테이션을 사용한다.

  • 억제할 수 있는 경고 메시지의 종류는 다음과 같다.
    • deprecation - @Deprecated가 붙은 대상을 사용해서 발생하는 경고를 억제
    • unchecked - 검증되지 않은 연산자를 사용할 때 발생하는 경고를 억제
      ArrayList list = new ArrayList(); // 제네릭스로 타입을 지정하지 않음
      list.add(obj); // 여기서 검증되지 않은 연산자(제네릭스를 사용하지 않은 ArrayList의 add)를 사용하여 unchecked 경고 발생
      PS C:\...> javac -Xlint AnnotationExample.java
      AnnotationExample.java:9: warning: [unchecked] unchecked call to add(E) as a member of the rawtype ArrayList
      list.add(1);
              ^
      where E is a type-variable:
      E extends Object declared in class ArrayList
      ...
      @SuppressWarnings("unchecked") // unchecked 경고 억제
      class AnnotationExample {
        public static void main(String[] args) {
          ArrayList list = new ArrayList(); // 제네릭스로 타입을 지정하지 않음
      	list.add(obj); // 여기서 unchecked 경고 발생
        }
      }
      PS C:\...> javac -Xlint AnnotationExample.java
      // unchecked 경고가 사라짐
      ...
    • rawtypes - 제네릭스를 사용하지 않아서 발생하는 경고를 억제
      ArrayList list = new ArrayList(); // 제네릭스를 사용하지 않음, 여기서 rawtypes 경고 발생
      PS C:\...> javac -Xlint AnnotationExample.java
      AnnotationExample.java:8: warning: [rawtypes] found raw type: ArrayList
          ArrayList list = new ArrayList();
          ^
      missing type arguments for generic class ArrayList<E>
      where E is a type-variable:
        E extends Object declared in class ArrayList
      		AnnotationExample.java:8: warning: [rawtypes] found raw type: ArrayList
            ArrayList list = new ArrayList();
                                 ^
      missing type arguments for generic class ArrayList<E>
      where E is a type-variable:
      E extends Object declared in class ArrayList
      @SuppressWarnings("rawtypes") // rawtypes 경고 억제
      ArrayList list = new ArrayList(); // 제네릭스를 사용하지 않음, 여기서 rawtypes 경고 발생
      PS C:\...> javac -Xlint AnnotationExample.java
    • varargs - 가변인자 타입이 제네릭 타입일 때 발생하는 경고를 억제
  • 경고 메시지의 종류를 애노테이션 뒤 괄호()안에 문자열로 지정하면 된다.
    @SuppressWarnings("deprecation")
  • 둘 이상의 경로를 동시에 억제하려면, 애노테이션 뒤 괄호()안에 배열처럼 괄호{}를 추가로 사용해야한다.
    @SuppressWarnings({"deprecation", "unchecked", "varargs"})
  • 억제할 수 있는 경고 메시지의 종류는 JDK의 버전이 올라가면서 계속 추가될 것이기 때문에, 이전 버전에서는 발생하지 않던 경고가 새로운 버전에서는 발생할 수 있다.
    새로 추가된 경고 메시지를 억제하려면, 경고 메시지의 종류를 알아야 하는데 -Xlint 옵션으로 컴파일해서 나타나는 경고의 내용 중에서 대괄호[] 안에 있는 것이 바로 메시지의 종류이다.
    PS C:\...> javac -Xlint AnnotationExample.java
    AnnotationExample.java:8: warning: [deprecation] getDate() in Date has been deprecated
            System.out.println(date.getDate());
                                   ^
    1 warning
  • 만약, 넓은 범위(클래스나 메소드) 앞에 @SuppressWarnings 애노테이션을 추가하면, 나중에 추가된 코드에서 발생할 수도 있는 경고까지 억제될 수 있으므로 해당 대상에만 @SuppressWarnings 애노테이션을 붙인다.

@Native

네이티브 메소드에 의해 참조되는 상수 필드에 붙이는 애노테이션

@Native public static final long MIN_VALUE = 0x8000000000000000L; 

🤔네이티브 메소드?
JVM이 설치된 OS의 메소드이다.

  • 보통 C언어로 작성되어 있는데, 자바에서는 메소드의 선언부만 정의하고 구현은 하지 않는다.
  • 네이티브 메소드를 호출하면 실제로 호출되는 것은 OS의 메소드이다.
  • 사용자 정의의 네이티브 메소드를 사용할 때는 반드시 해당 네이티브 메소드와 OS의 메소드를 연결해주는 작업이 추가로 필요하다.
    • JNI(Java Native Interface)가 해당 역할을 한다.
  • 예시
    public class Object {
      private static native void registerNatives(); // 네이티브 메소드
      static {
        registerNatives();
      }
      protected native Object clone() throws CloneNotSupportedException;
      public final native Class<?> getClass();
      public final native void notify();
      public final native void notifyAll();
      public final native void wait(long timeout) throws InterruptedException;
      public native int hashCode();
    }

메타 애노테이션

애노테이션을 위한 애노테이션
즉, 애노테이션에 붙이는 애노테이션으로 애노테이션을 정의할 때 애노테이션의 적용대상(target)이나 유지기간(retention)을 지정한다.

  • java.lang.annotation 패키지에 포함되어 있다.

@Target

애노테이션이 적용되는 대상을 지정하는데 사용된다.

  • 대상의 종류는 다음과 같다.

    대상 타입의미
    ANNOTATION_TYPE애노테이션. 즉, 메타 애노테이션을 의미함
    CONSTRUCTOR생성자
    FIELD필드(멤버변수, enum 상수)
    METHOD메소드
    PARAMETER매개변수
    LOCAL_VARIABLE지역변수
    PACKAGE패키지
    TYPE타입(클래스, 인터페이스, enum)
    TYPE_PARAMETER타입 매개변수(자바 8버전)
    TYPE_USE타입이 사용되는 모든 곳. 즉, 해당 타입의 변수를 선언할 때(자바 8버전)
    • java.lang.annotation.ElementType이라는 열거형에 정의되어 있다.
    • FIELD는 기본형에 TYPE_USE는 참조형에 사용된다.

다음은 @SuppressWarnings애노테이션의 정의이다.
@Target애노테이션으로 @SuppressWarnings의 적용 대상을 지정하였다.

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
...
public @interface SuppressWarnings {
  String[] value();
}

@Retention

애노테이션이 유지(retention)되는 기간

다음은 유지 정책의 종류이다.
java.lang.annotation.RetentionPolicy라는 열거형에도 정의되어 있다.

SOURCE

소스 파일에만 존재하고, 클래스파일에는 존재하지 않는다.

  • 컴파일러가 사용하는 애너테이션이다.
    • 따라서 컴파일러를 직접 작성할 것이 아니면, 이 유지 정책은 필요없다.
  • 예) @Override, @SuppressWarnings

CLASS

클래스 파일에 존재하고, 실행시에는 사용이 불가능하다.

  • 클래스 파일이 JVM에 로딩될 때는 애노테이션의 정보가 무시되어 실행 시에 애노테이션에 대한 정보를 얻을 수 없다.
    • 이것이 CLASS유지 정책이 기본값임에도 불구하고 잘 사용되지 않는 이유이다.

RUNTIME

클래스 파일에 존재하고, 실행시에 사용이 가능하다.

  • 실행 시에 reflection을 통해 클래스 파일에 저장된 애노테이션의 정보를 읽어서 처리할 수 있다.
  • 예) @FunctionalInterface : @Override처럼 컴파일러가 체크해주는 애노테이션이지만, 실행 시에도 사용되므로 유지 정책이 RUNTIME이다.

@Documented

@Documented가 붙은 애노테이션(D 애노테이션이라 하자)을 사용하는 대상이 D 애노테이션을 사용하고 있음을 javadoc에 표시해준다.

  • 자바에서 제공하는 기본 애노테이션 중에 @Override@SuppressWarnings를 제외한 모든 애노테이션에는 이 애노테이션이 붙어있다.

💡javadoc
자바에서 지정한 형태의 주석들을 인식하여 html로 된 api문서 형태를 만들어주는 도구이다.

@Inherited

@Inherited가 붙은 애노테이션이 자손 클래스에 상속되도록 한다.

@Repeatable

보통은 하나의 대상에 같은 이름의 애노테이션은 한 번만 붙이는데,
@Repetable이 붙은 애노테이션은 하나의 대상에 여러 번 붙일 수 있다.

따라서 일반적인 애노테이션과는 달리 같은 이름의 애노테이션이 하나의 대상에 여러번 적용될 수 있기 때문에, 이 애노테이션들을 하나로 묶어서 다룰 수 있는 애노테이션도 추가로 정의해야 한다.

@Repetable(ToDos.class) // ToDo 애노테이션을 하나의 대상에 여러 번 반복해서 사용할 수 있다.
@interface ToDo {
  String value();
}

// 여러 개의 ToDo 애노테이션을 담을 컨테이너 애노테이션 ToDos
@interface ToDos { 
  ToDo[] value(); // ToDo 애노테이션 타입의 배열을 선언. 이름이 반드시 value여야 한다.
}
@Todo("delete test codes.")
@Todo("override inherited methods.")
class MyClass {}

애노테이션 프로세서

애노테이션 프로세서란?

@RetentionSOURCE인 애노테이션은 컴파일러가 사용하는 애노테이션이라는 뜻이다.
해당 애노테이션을 사용하기위해서는 해당 애노테이션을 인식할 수 있는 컴파일러를 직접 구현해야 한다. 이때 컴파일러 플러그인의 일종으로 사용자 정의 애노테이션을 인식할 수 있도록 사용하는 것이 애노테이션 프로세서이다.
즉, 애노테이션 프로세서란 컴파일 타임에 특정 애노테이션을 스캔하고 처리하기 위해 javac에서 확장해서 사용하는 도구이다.

  • 애노테이션 프로세서 실행 과정은 다음과 같다.

    1. 컴파일러가 소스 파일에서 애노테이션을 검색한다.
    2. 해당 애노테이션에 적합한 애노테이션 프로세서를 선택한다.
    3. 애노테이션 프로세서를 실행해 특정 코드가 추가된 .java 파일이 새로 생성된다.
    4. 모든 애노테이션를 처리할 때까지, 다시 1번으로 돌아간다.
    • 이때, 1번에서의 소스 파일은 이전 애노테이션 프로세서가 실행된 후 생성된 .java 파일을 의미한다.
    • 1~3번까지 한바퀴를 돌면 한 라운드를 돌았다고 한다.
  • 해당 과정에서 주의할 점은 애노테이션 프로세서가 여러 개 있을 때, 프로세서의 실행 순서가 잘못 되어있을 경우 오류가 날 수 있다.

  • 애노테이션 프로세서의 대표적인 종류로는 Lombok, JPA, QueryDSL, MapStruct가 있다.

API로 애노테이션 프로세서 생성하기

AbstractProcessor라는 Processor 인터페이스를 구현한 추상클래스를 확장해서 자체적인 애노테이션 프로세서를 만들 수 있다.

  • AbstractProcessorjavax.annotation.processing 패키지안의 애노테이션 프로세서 API에 존재한다.
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@SupportedAnnotationTypes("*")
public class MyProcessor extends AbstractProcessor {

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

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
  }
}
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
    • 애노테이션 프로세서의 main() 메소드로, 애노테이션에 대한 스캔, 평가, 프로세싱과 자바 파일 생성에 대한 코드를 작성한다.
    • RoundEnvironment 파라미터는 애노테이션 프로세서가 애노테이션 프로세싱의 라운드를 쿼리할 수 있도록 해준다.
    • 반드시 구현해야하는 메소드이다.
  • @SupportedAnnotationTypes({애노테이션명1, ...})
    • 애노테이션 프로세서가 어떤 애노테이션들을 위해 만들어졌는지 애노테이션 이름의 집합을 작성한다.
    • 애노테이션 이름은 FQCN으로 적는다.
  • @SupportedSourceVersion(SourceVersion.RELEASE_11)
    • 어떤 자바 버전을 사용할지 정의한다.
    • 대부분 SourceVersion.latestSupported()를 사용하지만, 특정 버전을 정의하는 경우엔 SourceVersion.RELEASE_6과 같은 식으로 정의해줄 수 있다.
      • 하지만 전자의 방법을 권장한다.
  • init(ProcessingEnvironment processingEnv)
    • 모든 애노테이션 프로세서는 empty 생성자를 반드시 가져야하지만 annotation processing tool에 의해 실행되는 init()메서드를 사용하는 방법이 있다.
    • 인자로 제공되는 ProcessingEnvironment 타입은 몇가지 유용한 유틸리티 클래스인 Elements, Types, Filer를 제공한다.

생성한 애노테이션 프로세서 등록하기

  1. 다음과 같은 구조로 프로젝트를 생성해준다.
    MyProcessor.jar
      - dev
        - annotationprocessor
          - MyProcessor.class
      - META-INF
        - services
          - javax.annotation.processing.Processor
  2. javax.annotation.processing.Processor 파일에 애노테이션 프로세서 클래스들의 목록을 작성한다.
    dev.annotationprocessor.MyProcessor
    com.foo.BarProcessor
    ...
    • FQCN(Full Qualified Class Name)으로 적어준다.
  3. 프로젝트 export를 통해 jar 파일로 패키징한다.
  4. 이제 애노테이션 프로세서를 사용할 다른 프로젝트에서 빌드 경로에 MyProcessor.jar를 둔다.
  5. javac가 자동으로 javax.annotation.processing.Processor를 감지한다.
  6. 이를 읽은 다음 MyProcessor 애노테이션 프로세서를 등록한다.

애노테이션 프로세서 생성에 대한 자세한 설명은 다음 사이트를 참고하면 좋을 것 같다.

Reference

profile
안녕하세요.

0개의 댓글