본문 바로가기

WhiteShip Java Study : 자바 처음부터 멀리까지

Annotation

선장님과 함께하는 자바 스터디입니다.

자바 스터디 Github

github.com/whiteship/live-study

 

whiteship/live-study

온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub.

github.com

나의 Github

github.com/cmg1411/whiteShip_live_study

 

cmg1411/whiteShip_live_study

✍ 자바 스터디할래. Contribute to cmg1411/whiteShip_live_study development by creating an account on GitHub.

github.com

 

 

 

 

 

 

📚Annotation

에너테이션은 주석이라는 뜻이다. 기존의 우리가 알던 주석과는 다르긴 하다.

에너테이션은 코드에 대한 메타데이터를 코드에 직접적으로 기술할 수 있게 해준다. 이전에는 XML 을 통해 하던 것이었다.

 

메타데이터

메타데이터란 자신의 정보를 담고있는 데이터이다.

어노테이션은 코드 메타데이터로, 코드 자신에 대한 정보를 담는다. 패키지, 클래스, 메서드, 변수, 파라미터 등에서 사용하도록 지정할 수 있다.

 

에너테이션이 적용된 코드는 직접적으로 에너테이션의 코드를 실행하여 영향을 주는 것이 아니라, 단어의 뜻대로 [주석]으로써의 데이터를 제공한다.

 

따라서 런타임중에 필요한 값으로 돌아가는 코드는 못들어간다.

에너테이션의 코드는 완전히 정적이며, 컴파일타임에서 해석이 되어야 한다.

 

유즈 케이스

에너테이션은 여러가지 목적으로 사용된다. 그중 대표적으로는,

  • 컴파일러를 위한 정보 제공 : @FunctionalInterface, @SuppressWarnings..
  • 자동 문서 작성 : @Document..
  • 코드 자동 생성 : 롬복..
  • 런타임 프로세싱 : @Autowired..

 

📚Annotation 사용 방법

@RestController
public class HelloController {

    @GetMapping("/alkhwa")
    public String hello() {
        return HELLO;
    }
}

 

@

@ 이 소위 골뱅이라고 하는 문자가 이것이 에너테이션임을 표시한다.

 

이름

골뱅이 다음에는 에너테이션의 이름을 적는다.

 

요소

에너테이션은 값을 가질 수 있는데, 요소로 지정해줄 수 있다.

@Retention(value = RetentionPolicy.RUNTIME) 이런식으로 요소값을 인자 형태로 넘겨준다.

 

 

📚Built-in-Annotation

자바에서 기본으로 제공해주는 에너테이션들을 말한다.

 

대표적으로는 아래와 같은 예들이 있다.

  • Override
  • SuppressWarning
  • safeVarargs
  • Retension
  • Target
  • 등등

 

📚Annotation 정의 방법

빌트인 에너테이션에 적절한 에너테이션이 없을 경우, 사용자가 직접 만들어서도 쓸 수 있다.

 

에너테이션을 정의하는 방법은 클래스를 만드는 방법과 비슷한데, class 키워드를 @interface 키워드로만 바꿔주면 된다.

 

public @interface Hello {
}

 

아래는 위 코드의 바이트코드인데, 자동으로 Annotation 이라는 인터페이스를 구현하는 것을 볼 수 있다.

public abstract @interface com/whiteship/white_ship_study/week12/Hello 
    implements java/lang/annotation/Annotation {

}

 

요소값 지정 방법

  • 요소의 타입은 primitive type, String, enum, annotation, Class 만 허용한다.
  • 메서드 처럼 선언하며, () 안에 매개변수는 선언할 수 없다.
  • 예외를 선언할 수 없다.
  • 요소를 타입 매개변수로 정의 할 수 없다.

위의 Hello 에너테이션에 요소를 지정하려면 아래와 같이 한다.

 

public @interface Hello {
    String country() default "";
}
  • 명시적으로 지정되지 않았을 때의 기본값을 위해 default 를 붙일 수 있다.
  • 배열일 경우 {}, 문자열은 "" 이런 식으로 기본값을 지정한다.

 

에너테이션을 사용 할 때에는 아래와 같이 써야 한다.

@Hello(country = "Korea")
public class HelloUser {
}

 

 

기본요소 value

value 라는 이름으로 요소를 지정하면, 값을 전달할 때 이름을 생략할 수 있다.

public @interface Hello {
    String value();
}
@Hello("Korea")
public class HelloUser {
}

 

선언할 요소가 두개 이상이라면, 원래처럼 이름을 붙여야 한다.

@Hello(value = "greeting", country = "Korea")
public class HelloUser {
}

 

 

 

 

📚Annotation 에 지정된 정보들을 어떻게 적용할까 ?

리플렉션은 주석이다. 무언가를 실행하는 로직이 없고, 코드에 영향을 미치지 않는다.

에노테이션에 지정된 정보들은 어떻게 결과가 나오는 것일까.

 

그것은 시스템이나 서드파티 어플리케이션이 이 어노테이션들을 보고, 어떤 코드에 적용되었는지를 보고 적절한 행동들을 자동으로 하게 한다.

 

자바 빌트인 에너테이션들은 자바의 JVM 이 보고 각각에 적절한 행동을 수행한다.

Spring, JUnit 등의 프레임워크도 이 역할을 수행한다.

 

JVM, Spring, JUnit 같은 것들은 자바 리플렉션을 이용하여 에너테이션들을 읽어와서 실행하게 된다.

아래 실습을 보자.

 

// 후에 설명하겠지만, 어노테이션의 적용범위를 RUNTIME 으로 해야 런타임에서 에너테이션을 읽을 수 있다.
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Hello {
    String value() default "";
}
@Hello("Korea")
@RestController
public class HelloController {

}

실험용 컨트롤러 클래스를 만들고 클래스에 에너테이션을 붙인다.

 

그리고 에너테이션을 읽어들이는 main 코드를 작성한다.

public class HelloMain {

    public static void main(String[] args) {
    //        Annotation[] myAnnotations2 = HelloController.class.getDeclaredAnnotations(); 
    //        딱 그 클래스에 정의된 것만 보고 싶을 떄 = getDeclared~
        Annotation[] annotations = HelloController.class.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }
    }
}

 

이렇게 Class<> 클래스와 리플렉션을 이용하면 어떤 어노테이션을 붙였는지 알 수 있다.

 

 

여담이지만, Class<> 클래스에는 리플렉션을 이용한 여러 메서드들이 있는데,

getAnnotation() 처럼 get~ 는 외부에서 보이는, 즉 접근지정자에따라 접근가능한 것들을 가져온다.

그리고 상속관계가 있고, 에너테이션 선언에 @Inherited 어노테이션이 붙어있으면 상위 클래스까지도 찾는 범위로 포함한다.

 

반면에, getDeclaredAnnotation() 처럼 Declared 가 붙은 메서드들이 있는데,

이 메서드들은 해당 클래스에 선언된 정보를 얻어온다. 상속관계에 있어도 상위 클래스가 찾는 범위에 포함되는 것이 아니라 딱 그 클래스에 선언된 것들만 가져온다.

그리고 private 이라도 선언되어 있으면 찾아온다.

 

 

 

📚Java의 built-in Annotation 들

 

Built-in Annotation 의 패키지적 분류

자바는 여러가지 에너테이션을 제공하고 있는데, 이 에너테이션들은 두가지로 나눌 수 있다.

  • java.lang 패키지의 에노테이션
    • 컴파일러를 위해 붙이는 에너테이션
  • java.lang.annotation 패키지의 에노테이션
    • 메타 에너테이션으로, 에너테이션 정의에 사용하는 에너테이션

(java 8 의 경우 jdk > rt.jar > java > lang 과  jdk > rt.jar > java > lang > annotation 에서 확인할 수 있다.)

 

 

 

Built-in Annotation 의 기능적 분류

  • Marker 에너테이션 : 멤버변수가 없고, 컴파일러에게 의미 전달을 위해 사용하는 에너테이션 (Override, Deprecated 같은 것)
  • Single-Value 에너테이션 : 요소를 하나만 가지는 에너테이션. 보통 value() 로 사용하여 이름을 생략 가능.
  • Full 에너테이션 : 둘 이상의 요소를 가지는 에너테이션. 이름 = 값 형태를 모두 써줘야 한다. (default 사용 안했을 때)

 

 

 

java.lang 패키지의 에노테이션

@Deprecated

  • Marker 에너테이션.
  • 설계상 문제가 있거나, 다음 버전부터 사라질 예정이기 때문에 사용하지 말라고 경고한다.
  • 해당 에너테이션이 붙은 메서드들은 사용시에 줄이 쫙 그인다.
  • 하위 호환성 때문에 바로 삭제는 못하고 이 에너테이션으로 경고를 충분히 한 후 적절한 시기에 삭제하는 식이다.

Deprecated 된 isJavaLetterOrDigit 메서드
@Deprecated 가 붙어있다. 구현부의 Javadoc 을 보면 보통 왜 @Deprecated 가 붙었는지 적혀있다. 이 메서드에서는 다른 메서드로 교체된단다.
mac, intellij 기준으로 Deprecated 구현부에서 command + b 를 누르면 어떤 메서드가 Deprecated 되었는지 찾아준다.

 

 

@FunctionalInterface

  • 자바 8 버전 부터 나온 함수형 인터페이스와 같이 나온 어노테이션이다.
  • 마커 에너테이션으로, 해당 인터페이스가 함수형 인터페이스임을 알려준다.
  • 인터페이스에 붙인다.
  • 함수형 인터페이스가 아닌 경우 (메서드가 두개거나) 에러를 표시한다. (함수형 인터페이스인지 체크해줌)

 

not FunctionalInterface
FunctionalInterface
not FunctionalInterface

 

 

 

@Override

  • 마커 에너테이션. 
  • 메서드에 붙인다.
  • 해당 함수가 부모 클래스의 메서드를 재정의한 메서드인지 명시한다.
  • 오타, 부모 클래스에 없는 메서드 등의 이유로 재정의가 아닌 경우를 체크할 수 있다.

 

@SafeVarargs

  • 마커 에너테이션.
  • 제네릭과 배열은 궁합이 좋지 않은 편인데, 가변인자는 내부적으로 배열을 사용한다.
  • 따라서 컴파일러가 경고를 띄우게 된다.
  • 해당 경고는 무시해도 되며 안전한 가변인자임을 컴파일러에 알리는 매개변수이다.
  • 자세한 내용은 Effective Java 3E 5장과 item32 에서 볼 수 있다.

경고를 띄운다.
해당 메서드에 에너테이션을 붙이면 경고를 없앨 수 있다.

가변인수와 제네릭 타입은 궁합이 좋지 않기에, @SafeVarargs 를 붙일 이유가 있어야 하고, 무조건 붙이고, 붙일 수 없는 경우는 없게 하라는 Effective Java 의 조언이 있다.

 

 

몇일 전에 스터디원들과 <?>, <T> 에 대한 열띤 토론을 했었는데, 실습중에 <?> 를 쓰면 위의 경고가 뜨지 않았다. Effective Java 에서 <?> 는 실체화가 된다고 했던게 생각났고 그래서 안뜨는 것을 깨달았다.

<E> 와 <?> 의 차이가 가변 인자에서도 나오는 것을 깨달았다. (소소한 꺠달음 .. ㅎ)

 

 

 

 

@SuppressWarning

  • 컴파일러가 던지는 경고를 무시할 수 있다.
  • value 라는 하나의 요소를 가지고 있다.
  • 매개변수에 따라 무시할 수 있는 경고를 지정할 수 있다. 매개변수의 종류는 아래들이 있으며, 아래 외에도 많다.

컴파일러가 경고를 던지는 상황은 아주 많아서, 경고 문구를 보고 골라서 그때그때 쓰면 될 것 같다.

경고를 무시하는 이유는 꼭 있어야한다.

 

 

java.lang.annotation 패키지의 에노테이션

@Target

  • 에너테이션을 적용할 수 있는 대상들을 정한다.
  • 요소는 ElementType[] value(); 이다. 즉, 요소 타입을 배열로 가지고 있다.
  • 따라서 여러개를 {} 로 묶어 배열로 만들고, 요소로 전달할 수 있다.
  • 요소지정은 필수이다.
  • 어떤 옵션을 줄 수 있는지는 요소 타입인 Element 를 보면 될 것이다.
@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();
}
public enum ElementType {
    TYPE, // 클래스, 인터페이스

    FIELD, // 필드

    METHOD, // 메서드
    
    PARAMETER, // 매개변수

    CONSTRUCTOR, // 생성자

    LOCAL_VARIABLE, // 지역변수

    ANNOTATION_TYPE, // 에너테이션

    PACKAGE, // 패키지

    TYPE_PARAMETER, // 타입 파라미터 (제네릭)

    TYPE_USE
}

ElementType 이 enum 이기 때문에 정적으로 접근해야 한다.

따라서 아래와 같이 쓸 수 있다.

 

@Target({ElementType.TYPE, ElementType.METHOD})
@interface CustomAnnotation {
    
}

이 어노테이션은 클래스, 메서드에 붙일 수 있다고 선언되었다.

 

아래를 보면, 필드에 붙였을 시 에러가 나는 것을 알 수 있다.

 

 

@Retention

  • 에너테이션이 적용되는 범위를 정할 수 있다.
  • RetentionPolicy value(); 라는 요소를 가지고 있다.
  • RetentionPolicy 는 enum 이고, 세가지 상수가 있다.
  • 요소 지정은 필수이고, 이 에너테이션을 아얘 쓰지 않으면 CLASS policy 가 지정된다.
public enum RetentionPolicy {
    SOURCE,
    CLASS,
    RUNTIME
}

 

  • SOURCE : 컴파일 후 에너테이션 정보 없어짐(ByteCode 에 없음, @Override 같은거)
  • CLASS : 에너테이션에 대한 정보를 바이트코드에도 남김 (클래스 정보에 들어간다.) (런타임 중에 바이트코드 정보를 읽는 것도 Byte Buddy 같은걸로 가능함.)
  • RUNTIME : 런타임에는 클래스정보를 클래스로더가 읽어서 메모리에 적재. 그 때 에너테이션 정보를 누락시킴. 리플렉션 안됨. 리플렉션되게 할거면 RUNTIME

필요한 범위에 맞게 선언하도록 한다.

 

간단하게 확인해보자.

@RetentionAnnotation
public class RetentionEx {
    public static void main(String[] args) {
        System.out.println();
    }
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
@interface RetentionAnnotation {
}

RetentionPolicy 를 SOURCE 로 했다.

// class version 52.0 (52)
// access flags 0x21
public class com/whiteship/white_ship_study/week12/annotations/RetentionEx {

  // compiled from: RetentionEx.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week12/annotations/RetentionEx; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
    // parameter  args
   L0
    LINENUMBER 11 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    INVOKEVIRTUAL java/io/PrintStream.println ()V
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

 

 

그 바이트코드는 이와 같다. 다른 것도 봐야 비교가 가능할 것 같다.

@RetentionAnnotation
public class RetentionEx {
    public static void main(String[] args) {
        System.out.println();
    }
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
@interface RetentionAnnotation {
}

이 버전은 CLASS 로 지정했다. 바이트코드는 아래와 같다.

// class version 52.0 (52)
// access flags 0x21
public class com/whiteship/white_ship_study/week12/annotations/RetentionEx {

  // compiled from: RetentionEx.java

  @Lcom/whiteship/white_ship_study/week12/annotations/RetentionAnnotation;() // invisible

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week12/annotations/RetentionEx; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
    // parameter  args
   L0
    LINENUMBER 11 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    INVOKEVIRTUAL java/io/PrintStream.println ()V
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

 

 

차이점이 보이는가?

CLASS 로 지정하니 SOURCE 에 없던

 @Lcom/whiteship/white_ship_study/week12/annotations/RetentionAnnotation;() // invisible

이 부분이 생겼다.

그럼 처음 SOURCE 에서는 바이트코드에 기록이 안남고 컴파일 타임에만 에너테이션이 쓰인다는 것을 유추할 수 있다.

 

그럼 RUNTIME 은?

@RetentionAnnotation
public class RetentionEx {
    public static void main(String[] args) {
        System.out.println();
    }
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface RetentionAnnotation {
}
// class version 52.0 (52)
// access flags 0x21
public class com/whiteship/white_ship_study/week12/annotations/RetentionEx {

  // compiled from: RetentionEx.java

  @Lcom/whiteship/white_ship_study/week12/annotations/RetentionAnnotation;()

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week12/annotations/RetentionEx; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
    // parameter  args
   L0
    LINENUMBER 11 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    INVOKEVIRTUAL java/io/PrintStream.println ()V
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

 

 

자세히 보면 // invisable 주석이 사라진 것을 알 수 있다.

invisable이 무엇인가. 보이지 않는 이라는 뜻 아닌가.

CLASS 는 바이트코드에는 있지만 런타임에는 보이지 않는다는 것을 알려주고 있다.

반면, RUNTIME 은 주석이 없으므로 바이트코드를 가지고 런타임에 돌릴때도 정보가 남아있음을 유추할 수 있다.

 

 

@Inherited

  • 요소는 없다.
  • 마크 에너테이션으로, 해당 에너테이션이 붙은 클래스의 자식 클래스에도 같은 에너테이션 정보를 사용한다.
  • 말로는 헷갈릴 수 있으니 아래 코드를 보자.
public class InheritedEx {
    public static void main(String[] args) {
        Annotation[] annotations = B.class.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }
    }
}

@InheritedAnnotation
class A {
}

class B extends A {
}

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface InheritedAnnotation { // 커스텀 에노테이션
}

A 클래스에 커스텀 에너테이션을 정의했다.

커스텀 에너테이션은 @Inherited 가 붙어 있다.

 

그렇다면 A 를 상속받는 B 에도 커스텀 에너테이션이 붙은 것과 마찬가지가 된다.

(리플렉션을 통해 런타임에 에너테이션의 정보를 얻으려면 Retention이 RUNTIME 이어야 한다.)

 

B 에너테이션들을 가져와서 출력을 하는 코드를 실행시키면, 커스텀 에너테이션의 정보를 가져오는 것을 알 수 있다.

 

 

 

@Documented

  • javadoc 에 에너테이션 정보를 문서화할 수 있다.
  • 이것도 한번 해보면서 기능을 확인해보자.
public class DocumentedEx {
    @Document("에너테이션 있음")
    public void method1() {
    }

    @noDocument("에너테이션 없음")
    public void method2() {
    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@interface noDocument {
    String value();
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@interface Document {
    String value();
}

한 메서드에는 Document 에너테이션이 붙은 에너테이션을 붙였고,

다른 메서드에는 그렇지 않은 에너테이션을 붙였다.

 

javadoc 을 만들고 확인해보면, 차이를 볼 수 있다.

하나는 에너테이션 정보가 나타나고,

다른 것은 에너테이션 정보가 없다.

 

 

@Native

  • 필드의 값이 native 코드로부터 참조될 수 있음을 나타내는 마커 에너테이션.

 

 

@Repeatable

  • 에너테이션을 반복 정의할 수 있게 해준다.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    /**
     * Indicates the <em>containing annotation type</em> for the
     * repeatable annotation type.
     * @return the containing annotation type
     */
    Class<? extends Annotation> value();
}
  • 반복해서 에너테이션을 사용 시에 해당 값을 저장할 에너테이션을 Class<> 형식으로 지정한다.

 

 

 

📚에너테이션 프로세서

  • 컴파일 타임에 코드를 조작하기 위한 기술이다.
  • 어노테이션을 찾아서 소스코드를 조작할 수 있다.
  • 소스코드를 조작한다는 것은 새로운 자바 코드를 컴파일타임에 만든다는 것이다.
  • 컴파일타임에 코드가 만들어지므로, 런타임 비용이 없다는 장점이 있다.
  • 기존에 존재하는 코드에 대한 변경은 불가능하다.
  • 대표적인 예로 롬복이 있다.
  • 특정 에너테이션을 위한 에너테이션 프로세서를 등록 해야한다.
  • 소스코드를 만들어 내는 것 외에도, 컴파일 에러나 경고를 출력하거나 바이트코드를 만들기도 한다.
  • 커스텀 processor 를 만들기 위해서는 AbstractProcessor 를 상속받아야 한다.

 

에너테이션 프로세서 동작 구조

1. 어노테이션 클래스를 생성한다.

2. 어노테이션 파서 클래스를 생성한다.

3. 어노테이션을 사용한다.

4. 컴파일하면, 어노테이션 파서가 어노테이션을 처리한다.

5. 자동 생성된 클래스가 빌드 폴더에 추가된다.

 

프로세스 중 파일이 생성되면 생성된 파일을 입력으로 하는 다른 단계 프로세스가 실행된다. 아래 process 메서드의 리턴타입이 boolean 인 이유이다. 프로세서 처리 단계에서 파일이 더 이상 생성되지 않을 때 까지 계속된다. 즉 return 이 true 일 때까지 계속된다.

 

AbstractProcessor 주요 메서드

  • init : 초기화 작업을 할 수 있으며, ProcessingEnvironment 를 초기화 한다. (한번만 가능), 재정의 하지 않을 시 기본 메서드가 실행
    public synchronized void init(ProcessingEnvironment processingEnv) {
        if (initialized)
            throw new IllegalStateException("Cannot call init more than once.");
        Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");

        this.processingEnv = processingEnv;
        initialized = true;
    }

 

  • process : 에너테이션을 읽어들여 수행할 로직을 작성하고, 실행하는 메서드. 추상 메서드로 선언되어 있어 재정의가 필수.
    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);

 

  • getSupportedAnnotationTypes() : 에너테이션 프로세서가 담당할 에너테이션을 등록하는 메서드. 반환값은 FQN 이어야 한다. 재정의하지 않을시 기본 메서드.
    public Set<String> getSupportedAnnotationTypes() {
            SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
            if  (sat == null) {
                if (isInitialized())
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                             "No SupportedAnnotationTypes annotation " +
                                                             "found on " + this.getClass().getName() +
                                                             ", returning an empty set.");
                return Collections.emptySet();
            }
            else
                return arrayToSet(sat.value());
        }

 

  • getSupportedSourceVersion() : 사용할 자바 버전을 지정한다. 
    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion sv = null;
        if (ssv == null) {
            sv = SourceVersion.RELEASE_6;
            if (isInitialized())
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                         "No SupportedSourceVersion annotation " +
                                                         "found on " + this.getClass().getName() +
                                                         ", returning " + sv + ".");
        } else
            sv = ssv.value();
        return sv;
    }

 

 

 

에너테이션 프로세서 등록하는 방법

위에서도 언급했듯, 에너테이션 프로세서를 등록해야 한다. 컴파일타임에 이루어지므로 javac 에 등록한다.

  1. 에너테이션 프로세서(프로젝트) 를 jar 파일로 만든다.
  2. 에너테이션 프로세서를 사용할 프로젝트에 그 jar 파일을 불러온다. (dependency 설정)
  3. META-INF/services 폴더를 만든다.
  4. javax.annotation.processing.Processor 라는 이름의 File 을 만든다.
  5. 위 파일에 에너테이션 프로세서의 FQCN 을 입력한다.
  6. 등록을 확인한다.

 

 

 

실습

에너테이션을 클래스에 붙이면, 인자에 넘기는 값에 맞는 클래스와 메서드를 생성하는 실습을 해볼 것이다.

 

1. 새 프로젝트를 만들고, 에너테이션을 만든다.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BeanRegister {
    ClassType value() default ClassType.COMPONENT;
}

 

 

2. 인자값으로 쓸 enum 을 만든다.

public enum ClassType {
    COMPONENT,
    CONTROLLER,
    SERVICE,
    REPOSITORY
}

 

 

3. Processor 클래스를 만든다.

javapoet 라는 라이브러리를 이용하였다.

.java 파일을 손쉽게 만들어주는 라이브러리이다.

public class BeanRegisterProcessor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Stream.of(BeanRegister.class.getName())
            .collect(Collectors.toSet());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

    /**
     * return type 이 true 이기 때문에, 이 메서드로 해당 에너테이션의 처리를 완료했다는 것을 알린다.
     * false 면 다음 processor 에게 처리를 넘겨야 함을 뜻한다.
     * 다음 프로세서에서도 처리가 필요한 경우 false 를 반환한다.
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // @BeanRegister 가 붙어있는 elements 를 읽어옴
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BeanRegister.class);

        // 반복문을 돌면서 하나하나 에너테이션에 대한 작업 실행
        for (Element element : elements) {
            Name elementName = element.getSimpleName();

            // 에너테이션이 붙어있는 곳이 CLASS 가 아니면 에러메세지 출력
            if (element.getKind() != ElementKind.CLASS) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Bean cannot registered with " + elementName);
            }
            if (element.getKind() == ElementKind.CLASS) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + elementName);
            }

            TypeElement typeElement = (TypeElement) element;
            // JavaPoet 라이브러리 사용
            ClassName className = ClassName.get(typeElement);

            // 요소값 가져오
            ClassType classType = element.getAnnotation(BeanRegister.class).value();

            // 메서드 만들기
            MethodSpec print = MethodSpec.methodBuilder("print")
                                        .addModifiers(Modifier.PUBLIC)
                                        .returns(String.class)
                                        .addStatement("System.out.println($S)", classType)
                                        .addStatement("return $S", classType)
                                        .build();

            // 메서드를 포함하는 클래스 만들기
            TypeSpec module = TypeSpec.classBuilder("Module" + elementName)
                                    .addModifiers(Modifier.PUBLIC)
                                    .addMethod(print)
                                    .build();

            // 실제 class 파일을 만듦
            Filer filer = processingEnv.getFiler();
            try {
                JavaFile.builder(className.packageName(), module)
                    .build()
                    .writeTo(filer); // 위에서 만든 클래스 파일을 write
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR : " + e);
            }
        }
        return true;
    }
}

 

4. 빌드 도구로 compile

 

5. 프로세서 등록

resource 에 META-INF/services 디렉토리를 만들고, javax.annotation.processing.Processor 파일을 만든다.

이 안에 만든 Processor 클래스의 FQCN 을 넣는다.

 

Google의 auto-service 라이브러리를 사용하면 javax.annotation.processing.Processor 파일을 만드는 과정을 쉽게 할 수 있다.

 

6. jar 파일 만들기

maven 의 경우 mvn install

 

프로젝트의 구조는 다음과 같다.

 

 

 

7. 에너테이션을 사용할 프로젝트에서 의존성 주입

라이브러리가 들어왔다.

 

 

 

8. 에너테이션을 사용하는 코드 작성

요소값으로 COMPONENT 를 지정했다.

이 코드를 컴파일한다.

@BeanRegister(ClassType.COMPONENT)
public class UseBeanRegister {
    public static void main(String[] args) {
    }
}

 

 

9. 코드가 생성됬는지 확인

의도한대로 잘 만들어졌다.

 

 

 

10. 코드 테스트

@BeanRegister(ClassType.CONTROLLER)
public class UseBeanRegister {
    public static void main(String[] args) {
        ModuleUseBeanRegister moduleUseBeanRegister = new ModuleUseBeanRegister();
        moduleUseBeanRegister.print();
    }
}

 

인자값을 바꾸면서 테스트해보면 인자값에 따라 만들어지는 파일이 다르고, 실행결과도 다름을 알 수 있다.

 

 

 

 

 

 

마지막으로, 롬복의 구성과 롬복을 사용했을때 어떤 코드가 생성되는지 알아보면 좋을 것이다.

롬복은 새로운 코드를 생성하는 것이 아닌 AST 를 바꾸는데, 원래는 참조만 가능한 것을 바꾸니 해킹이라 하는 사람들도 있다고 한다.

 

 

 

 

 

참조:

hamait.tistory.com/314?category=79137

github.com/ByungJun25/study/tree/main/java/whiteship-study/12week

blog.naver.com/swoh1227/222229853664

www.notion.so/12-95595cad188b45058bfb1ddcf97869c5

velog.io/@ljs0429777/12%EC%A3%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C-%EC%95%A0%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98#%EC%95%A0%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%ED%94%84%EB%A1%9C%EC%84%B8%EC%84%9C-

www.notion.so/12-a4b39805e8c045729e2167e88088a7f9

github.com/JeongJin984/JeongJin984.github.io/blob/master/_posts/2021-02-26-Java-Annotation.md

github.com/gtpe/java-study/blob/master/w12.md

blog.naver.com/hsm622/222226824623

 

 

 

'WhiteShip Java Study : 자바 처음부터 멀리까지' 카테고리의 다른 글

I/O  (0) 2021.02.17
ServiceLoader  (0) 2021.02.14
enum  (0) 2021.02.08
멀티스레드 프로그래밍  (0) 2021.02.06
예외 처리  (0) 2021.02.05