티스토리 뷰

Annotation Processor란?

Annotation Processor는 컴파일 단계에서 Annotation에 정의된 일렬의 프로세스를 동작하게 하는 것을 의미합니다. 

컴파일 단계에서 실행되기 때문에, 빌드 단계에서 에러를 출력하게 할 수 있고, 소스코드 및 바이트 코드를 생성할 수도 있습니다.

사용하는 예로 자바의 @Override가 있으며, Lombok(롬북)이라는 라이브러리도 있습니다.

Lombok은 자주 사용하는 라이브러리로 한번 살펴보겠습니다.

 

Lombok이란?

@Getter, @Seteer, @Builder 등의 Annotation과 Annotation Processor를 제공하여 표준적으로 

작성해야 할 코드를 개발자 대신 생성해주는 라이브러리 입니다.

컴파일 시점에 Annnotation Processor를 사용하여 abstract syntaxtree를 조작합니다.

그러면, 프로젝트를 생성하여 Lombok 라이브러리를 실습해보겠습니다.

 

Preferences -> Plugins설정에서 IntelliJ Lombok 플러그인을 설치합니다.

Preferences -> Build,Execution,Deployment -> Compiler -> Annotation Processors설정에서

Enable Anntation processing 체크박스를 활성화합니다.

Lombok Annotation을 사용하는 클래스는 변경할 때마다 그대로 적용이 되어 Maven Complie을 안 해도 되는 설정입니다.

 

<dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.8</version>
      <scope>provided</scope>
</dependency>

pom.xml에 lombok 라이브러리 dependency를 추가합니다.

 

package me.whiteship;


import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@Getter @Setter @EqualsAndHashCode
public class Member {

    private String name;

    private int age;
}

@Getter : 전역 변수에 대한 Getter를 생성합니다.

@Setter : 전역 변수에 대한 Setter를 생성합니다.

@EqualsAndHashCode : Equals 함수와 HashCode 함수를 재정의하여 생성합니다.

package me.whiteship;

import org.junit.Assert;
import org.junit.Test;

import static org.junit.Assert.*;

public class MemberTest {

    @Test
    public void getterSetter(){
        Member member = new Member();
        member.setName("minkyu");

        Assert.assertEquals(member.getName(), "minkyu");
    }

}

소스코드에 getter 및 setter 함수를 개발자가 직접 만들지 않아도

setName 및 getName 함수를 쓸 수 있으며, 테스트 코드도 통과하는 것을 확인할 수 있습니다.

이제 Annotation Processor에 대해 살펴보겠습니다.

 

Processor Interface를 통하여 Annotation Processor를 구현하고,

Processor Interface는 여러 라운드(rounds)에 거쳐 Source 및 Compile 된 코드를 처리할 수 있습니다.

참조 : https://docs.oracle.com/en/java/javase/11/docs/api/java.compiler/javax/annotation/processing/Processor.html

Abstract Pocessor라는 추상 클래스를 상속하여 Annotation Processor를 구현해 보겠습니다.

 

Annotation Processor Jar 프로젝트를 생성해 보겠습니다.

 <!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
<dependency>
  <groupId>com.google.auto.service</groupId>
  <artifactId>auto-service</artifactId>
  <version>1.0.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.squareup/javapoet -->
<dependency>
  <groupId>com.squareup</groupId>
  <artifactId>javapoet</artifactId>
  <version>1.13.0</version>
</dependency>

AutoService 및  Javapoet 라이브러리를 추가해줍니다.

AutoService : Java SPI(서비스 프로바이더 인터페이스) 구성 파일을 생성하는데

도움이 되는 Annotation processor 라이브러리입니다.

컴파일 시점에 Annotaion Processor를 사용하여 

META-INF/services/javax.annotation.processor.Processor 파일 등 필요한 파일을 자동으로 생성해줍니다.

참고 : https://www.baeldung.com/google-autoservice

Javapoet : Source Code(.java)파일을 생성해주는 라이브러리입니다.

 

package me.whiteship;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

Annotation 범위는 Interface, Enum, Class, Annotation이고,

Annotation 유지는 Annotation Processor의 컴파일 시점까지만 필요하기 때문에

Source Code까지만 유지합니다.

 

package me.whiteship;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.Set;

@AutoService(Processor.class)
public class MagicMojaProcessor extends AbstractProcessor {

    // 이 프로세서가 어떤 애노테이션을 처리 할 것 인지 정하는 메소드
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Magic.class.getName());
    }
    
    // 어떤 소스버전을 지원 할지 정하는 메소드 
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    
    // 해당 애노테이션으로 작업을 처리하는 메소드
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {      
        // 해당 애노테이션이 붙어 있는 엘리먼트들을 가져 온다.
        // Element : 클래스, 인터페이스, 메소드 등 애노테이션을 붙일 수 있는 target
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for (Element element: elements) {
            // 해당 Element 이름
            Name simpleName = element.getSimpleName();

            // 해당 Element가 Interface가 아닌 경우 빌드에서 에러나게 동작
            if (element.getKind() != ElementKind.INTERFACE) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic annotation can not be used on " + simpleName);
            } else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + simpleName);
            }

            TypeElement typeElement = (TypeElement) element;
            ClassName className = ClassName.get(typeElement);

            // MethodSpec: Method 만드는 객체
            MethodSpec pullout = MethodSpec.methodBuilder("pullOut")
                    .addModifiers(Modifier.PUBLIC)          // 접근 제한자 설정
                    .returns(String.class)                  // Method return Type 설정
                    .addStatement("return $S", "Rabiit!")   // return 시 값 전달 설정
                    .build();

            // TypeSpec : Type 만드는 객체
            TypeSpec magicMoja = TypeSpec.classBuilder("MagicMoja")
                    .addModifiers(Modifier.PUBLIC)  // 접근 제한자 설정
                    .addMethod(pullout)             // 해당 클래스에 메소드 추가
                    .addSuperinterface(className)
                    .build();

            // Filer : 소스코드,클래스코드 및 리소스를 생성할 수 있는 인터페이스
            // processingEnv : AbstractProcessor 상속 받으면 쓸 수 있는 전역 변수
            Filer filer = processingEnv.getFiler();
            try {
                JavaFile.builder(className.packageName(), magicMoja)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR : " + e);
            }
        }

        /*
          true 일 경우 해당 애노테이션에 대한 다른 프로세서한테 더이상 처리하지 말라고 한다.
          false 일 경우 또 다른 프로세서가 처리 할 수 있음.
        */
        return true;
    }
}

Java Processing API를 살펴보겠습니다.

roundEnv.getElementsAnnotatedWith(Magic.class) : Magic Annotation이 붙어 있는 Element들을 가져옵니다.

Element : 클래스, 인터페이스, 메소드 등 애노테이션을 붙일 수 있는 target

  • getKind() : 해당 Type 종류를 반환합니다. ex) ElementKind.INTERFACE
  • getSimpleName() : 해당 Type의 이름을 반환합니다. ex) Interface 이름

Filer : Source Code, Class Code 및 리소스를 생성할 수 있는 인터페이스

 

AbstractProcessor의 구현한 메소드들을 살펴보겠습니다.

  • getSupportedAnnotationTypes() : 이 Processor가 어떤 Annotation을 처리할 것인지를 정의하는 메소드입니다.
  • getSupportedSourceVersion() : 어떤 자바버전을 지원할지 정의하는 메소드입니다.
  • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
    AbstractProcessor의 필수적으로 구현해야 하며, Processor가 어떻게 동작해야 하는지를 정의하는 메소드입니다.
    return 값이 true일 경우 해당 Annotation에 대한 다른 Processor한테 더 이상 처리하지 말라고 합니다.
    false일 경우 또 다른 Processor가 처리할 수 있습니다.

Javapoet 라이브러리를 이용한 객체들을 살펴보겠습니다.

MethodSpec : Method 정보를 가지고(생성하는) 있는 객체입니다.

  • MethodSpec.methodBuilder("pullOut") : 빌더 패턴 생성자로서,  pullOut이라는 메소드명을 정의합니다.
  • addModifiers(Modifier.PUBLIC) :  접근 제한자를 Public으로 정의합니다.
  • returns(String.class)  :Method return Type을 String으로 정의합니다.                   
  • addStatement("return $S", "Rabiit!")  : Method가 진행할 코드를 정의합니다.
  • build() : 위의 정의한 내용으로 인스턴스를 생성합니다.

TypeSpec : Type 정보를 가지고(생성하는) 있는 객체

  • TypeSpec.classBuilder("MagicMoja") : 빌더 패턴 생성자로서,  MagicMoja라는 클래스 명을 정의합니다.
  • addModifiers(Modifier.PUBLIC)  : 접근 제한자를 Public으로 정의합니다.
  • addMethod(pullout) : 클래스에 메소드를 추가합니다.
  • addSuperinterface(className) : 상속할 Interface를 추가합니다.
  • build() : 위의 정의한 내용으로 인스턴스를 생성합니다.

JavaFile : Source Code(.java)파일로 생성해주는 객체

  • builder(className.packageName(), magicMoja) : 빌더 패턴 생성자로서, 
    Source Code파일의 패키지명과 생성할 Type을 정의합니다. 
  • build() : 위의 정의한 내용으로 인스턴스를 생성합니다.
  • writeTo(filer) : 파일로 생성합니다.

프로젝트를 로컬 메이븐 저장소에 추가하여 사용하기 위해  maven clean install 명령어를 실행합니다.

이제 저희가 만든 Annotation Processor가 어떻게 동작하는지 확인하기 위해 

테스트할 프로젝트를 생성해보겠습니다.

 

<dependency>
      <groupId>we.whiteship</groupId>
      <artifactId>magicmoja</artifactId>
      <version>1.0-SNAPSHOT</version>
</dependency>

프로젝트 생성 후 pom.xml에 Annotation Processor 프로젝트의 groupId, artifactId, version 복사하여 dependency에 추가합니다.

package org.example;

import we.whiteship.Magic;

@Magic
public interface Moja {
    String pullOut();
}

저희가 만든 Magic Annotation을 Moja Interface에 적용합니다.

 

Preferences -> Build,Execution,Deployment -> Compiler > Annotation Processors설정에서

Enable annotation processing 체크박스를 활성화합니다.

annotation processing은 컴파일 시점에 Annotation을 스캔하고 처리하기 위해 자바 컴파일러에 삽입된 툴입니다.

컴파일 시점에서 annotation processing이 만든 클래스를 참조하기 위해 설정합니다.

 

Maven Compile 명령어를 실행합니다.

프로젝트 우클릭 -> Open Module Settings -> Project Settings -> Modules설정에서 

target -> generated-sources -> annotations폴더를 클릭하고 Sources폴더 그림을 클릭하여 활성화합니다.

Annotation Processor가 만든 클래스를 다른 클래스가 참조할 수 있게 소스 경로로 인식하게 해줘야 합니다.

 

package org.example;

public class App 
{
    public static void main( String[] args ) {
        Moja moja = new MagicMoja();
        System.out.println(moja.pullOut());
    }
}

 

실행하면  빌드 화면에서는 Processing Moja가 출력이 되고,

Run 화면에서는 Rabbit이 출력됩니다.

 

이로써 공부한 내용을 간략히 정리해보았습니다. 

감사합니다.


출처

https://www.inflearn.com/course/the-java-code-manipulation