[Java] Annotation Processor

hwhyeons·2025년 1월 13일

자바에는 getter, setter 메소드를 자동으로 만들어주는 lombok이라는 라이브러리가 존재한다.

나는 이 롬복이, 너무 당연하게 Java의 리플렉션을 이용해서 런타임에 코드를 수정한다고 무의식적으로 생각중이었다.

왜냐면 이전까지는 Java에서 컴파일 타임에 개발자가 개입해서 코드를 수정할 수 있다는 것을 몰랐다.

또한, Java Reflection도 자바의 어노테이션을 이용해 런타임에 특정 작업을 할 수 있다는 공통점이 있었기 때문에, 너무 자연스럽게
"lombok의 어노테이션도 리플렉션을 사용하는구나"
라고 생각해왔던 것 같다.


그러나, 롬복 관련된 정보를 찾아보고 스프링부트를 배우는 중에 순간적으로
이상함을 느꼈다.

롬복은 빌드하고 나서 @Getter 등은 이미 get~~()등의 메소드로 변환되어 있는 것으로 알고있는데, 빌드 과정은 컴파일이지 런타임 상황이 아니고,
그 뜻은 리플렉션이 아니라는 것이다.


lombok을 사용하는 클래스 파일을, jar 등으로 빌드해보면 알 수 있다.

.java 파일에서는 @Getter이 붙어 있던 파일이,
.jar 파일에서 컴파일된 .class파일을 열어보면,
@Getter 어노테이션은 없어지고, get 메소드로 이미 변경되어있다.

이를보고 롬복은 런타임이 아닌 컴파일타임에 개입한다는 것을 알게되었다.
lombok의 원리를 공부하면서 자바의 어노테이션 프로세서에 대해 공부하게 되었다.




Annotation Processor

Annotation Processor을 요약하면, 컴파일 타임에 특정 annotation이 붙어있는 코드를 이용해서 기존 코드를 변경할 수 있다.

정확히 말하면 AST(Abstract Syntax Tree)를 수정한다.
(파이썬에서는 AST를 많이 접해봤는데, 자바에서 언급 되는 것은 이때 처음 알게되었다)




직접 구현해보는 것이 가장 습득하기 좋을 것 같아서, 두가지 프로젝트를 만들었다.
1. 어노테이션 프로세서 라이브러리 사용할 프로젝트
2. 어노테이션을 사용할 프로젝트

두 프로젝트 모두 Gradle을 사용하였고, 두번째 프로젝트에서 첫번째 프로젝트를 gradle로 의존성을 추가하여 테스트 해볼 것이다.

테스트는 컴파일 타임에, 즉 jar 등을 빌드할 때 특정 클래스의 멤버를 읽어서
출력하는 것을 목표로 하였다 (실제 소스 코드 수정 등의 작업은 하지 않았음)



빠른 프로젝트를 위해서, Google의 AutoService를 이용했다.
이는 구글에서 제공하는 어노테이션 프로세서 라이브러리로,
처음에는 이를 사용하지 않고 구현을 시도했으나,
생각보다 설정해야되는 것들이 많아서 AutoService를 이용하기로 했다.

build.gradle에

annotationProcessor 'com.google.auto.service:auto-service:1.1.0'

를 추가해준다.


어노테이션을 구현할 인터페이스를 만들어준다.
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 TestAnno {

}

어노테이션 이름은 TestAnno이고, 이후 클래스에 @TestAnno
붙여서 테스트할 것이다.

이번에는 프로세서로 사용할 Java파일이 필요하다.

package ~~

import com.google.auto.service.AutoService;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.Set;

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_11) // 11버전
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(TestAnno.class);
        System.out.println("Hi annotation processor");
        for (Element element : elements) {
            System.out.println("이름 : "+element.getSimpleName());
            Set<Modifier> modifiers = element.getModifiers();
            modifiers.forEach(modifier -> System.out.println("\tmodifier : "+modifier));
            for (Element enclosedElement : element.getEnclosedElements()) {
                System.out.println("\t\tenclosedElement : "+enclosedElement.getSimpleName());
            }
        }

        return true;
    }
}

AbstarctProcessor을 상속받아 prcoess() 메소드를 구현하면 된다.
TestAnno 어노테이션을 가진 element를 전부 조회해서 이름을 출력한다.




첫번째 프로젝트를 빌드하고, 빌드된 jar파일 경로를 두번째 프로젝트의
gradle 의존성에 추가할 것이다.

dependencies {
    annotationProcessor files("~~.jar")
    implementation files("~~.jar")
}

아까 첫번째 프로젝트에서 빌드한 jar 파일을 위와 같이 설정해준다.


실제 어노테이션을 달아볼 클래스를 만들어서 붙여본다.
@TestAnno
public class NewClass {
    public void testMethod(){
        System.out.println("testMethod");
    }
}

클래스에는 메소드 하나만 넣어보았다.


이대로만 하면 어노테이션 프로세서가 동작하지 않기 때문에 인텔리제이에서 아래와 같이 Enable을 해준다.

이제 두번째 프로젝트를 빌드하면

이런식으로 메시지가 출력되는 것을 확인할 수 있다.

0개의 댓글