BackEnd/Java

[Java] Annotation Processor

샤아이인 2022. 6. 8.

 

이번시간에는 Lombok이 어떻게 동작하는지 그 핵심원리인 annotation processor에 대하여 학습해보자!

 

1. Lombok의 원리

annotation processor 는 compile 할때 중간에 끼어들어서 특정 annotation 이 붙어있는 소스코드를 참조하여 추가적인 소스코드를 삽입시킬 수 있다!

 

소스코드의 AST(abstract syntax tree)를 조작하는 것 이다!

https://javaparser.org/inspecting-an-ast/

 

Inspecting an AST

Inspecting an AST

javaparser.org

 

원래는 AST 의 정보는 참조만 할 수 있고, 소스 코드를  조작할수는 없다.

 

하지만 Lombok을 사용하면 compile된 바이트코드를 보면 마치 "소스 코드가 바뀌어 들어간것"처럼 보인다.

 

원래 공개된 open API 인 TypeElement, RoundEnvironment 만 사용해서 참조만 해야한다!

하지만 Lombok은 이를 하위타입으로 downCasting한 구체타입 중에서 AST를 수정을 할 수 있는 타입을 만들어서 처리한다.

따라서 이러한 Lombok은 사실한 해킹 기법의 일종이다. 정상적인 과정은 아니다!

 

2. Annotation Processor API

이번 시간에는 모자에서 토끼를 꺼내는 마술을 부려봅시다!

참고로 프로젝트 2개를 만들 것 입니다.

1) processor를 적용할 프로젝트

2) processor를 만들 프로젝트

 

 

우선 processor를 적용할 프로젝트에서 사용할 간단한 Interface는 다음과 같다.

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

main 코드는 다음과 같다.

public static void main(String[] args) {
    Hat hat = new HatFactory();
    System.out.println("hat 에서 나온것은! " + hat.pullOut());
}

여기서 HatFactory가 바로 우리의 annotation processor를 통하여 생성되는 것 이고,

그리고 annotation processor 로 처리할 에노테이션이 바로 @Magic 이다!

 

다음으로 processor를 만들 프로젝트 프로젝트를 따로 하나 만들자.

해당 프로젝트에 다음과 같은 에노테이션을 하나 만들자.

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

 

참고로 AutoService와 javapoet 을 사용할 것 이기 때문에 이에 대한 의존성을 추가하자.

https://mvnrepository.com/artifact/com.google.auto.service/auto-service/1.0.1

 

Maven Repository: com.google.auto.service » auto-service » 1.0.1

Provider-configuration files for ServiceLoader. com.google.auto.service auto-service 1.0.1 // https://mvnrepository.com/artifact/com.google.auto.service/auto-service implementation group: 'com.google.auto.service', name: 'auto-service', version: '1.0.1' //

mvnrepository.com

https://mvnrepository.com/artifact/com.squareup/javapoet/1.13.0

 

Maven Repository: com.squareup » javapoet » 1.13.0

Use beautiful Java code to generate beautiful Java code. com.squareup javapoet 1.13.0 // https://mvnrepository.com/artifact/com.squareup/javapoet implementation group: 'com.squareup', name: 'javapoet', version: '1.13.0' // https://mvnrepository.com/artifac

mvnrepository.com

 

annotation processor은 round라는 개념으로 작동하게 된다.

각 라운드 마다 processor 들에게 해당 processor 가 처리할 annotation을 찾으면 처리하게 된다.

처리된 결과가 다음 라운드로 넘어갈수도 있다.

 

 

Processor (Java SE 11 & JDK 11 )

The interface for an annotation processor. Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to th

docs.oracle.com

 

또한 true를 반환하는 경우 processor에서 처리되었기 때문에 다른 processor에서 해당 annotation을 다시 처리하지 않게된다.

 

이를 사용하는 Processor 코드는 다음과 같다.

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(Magic.class.getName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for (Element element : elements) {
            if (element.getKind() != ElementKind.INTERFACE) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic annotation을 " + element.getSimpleName() + "에서는 사용할 수 없습니다.");
            } else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + element.getSimpleName());
            }

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

            // 메서드 먼저 만들기
            MethodSpec pullOut = MethodSpec.methodBuilder("pullOut")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return $S", "Rabbit!")
                    .build();

            // class 만들기
            TypeSpec hatFactory = TypeSpec.classBuilder("HatFactory")
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(className)
                    .addMethod(pullOut)
                    .build();
            // 여기 까지하면 메모리상에 객체로 정의한것

            // 소스파일로 만들기
            Filer filer = processingEnv.getFiler();
            try {
                JavaFile.builder(className.packageName(), hatFactory)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "[FATAL ERROR]: " + e.getMessage());
            }
        }
        return true;
    }
}

getSupprotedAnnotationTypes() 를 통해 해당 processor가 처리할 annotation을 명시해주면 된다.

 

우리의 annotation은 interface에만 추가할수 있길 원한다. 다음 코드부분에서 이를 처리한다.

if (element.getKind() != ElementKind.INTERFACE) {
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic annotation을 " + element.getSimpleName() + "에서는 사용할 수 없습니다.");
} else {
    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + element.getSimpleName());
}

 

이후 processor를 만들 프로젝트의 pom.xml에서 processor를 적용할 프로젝트에 다음 부분을 복사하자

복사한 부분을 processor를 적용할 프로젝트에로 돌아가 의존성을 추가해준다.

이후 메이븐을 새로고침 해주면 다음과 같이 라이브러리에 들어와 있다.

 

processor를 적용할 프로젝트에로 이동하여 clean, build를 다시해보자.

그러면 다음과 같이 소스파일에 HatFactory가 생성된것을 확인할 수 있다.

이후 위 소스 파일이 있는 디렉토리를 소스파일 디렉토리로 인식시켜야 한다.

다음과 같이 annotations 디렉토리를 Sources 로 변경시켜주자.

이후 annotation processor 옵션을 선택해주자

이후 다시 clean build 를 해보자.

다음과 같이 processor를 적용할 프로젝트에서 사용이 가능하다.

원하던 결과인 hat에서 Rabbit이 나오게 되었다!

댓글