본문 바로가기
Java

[Java] Annotation Processor 사용법 & 예제

by 마진 2025. 6. 9.

 

어노테이션 프로세서(Annotation Processor)란? 

어노테이션 프로세서란 Java 파일을 컴파일하거나 프로젝트 빌드 시 선언된 어노테이션을 감지하고 그에따라 정해진 작업을 수행하는 도구를 의미한다. 이러한 프로세서는 코드 생성, 검증, 문서화 등 다양한 작업을 자동화할 수 있으며, 특히 반복적인 보일러플레이트 코드(Boilerplate Code)를 줄이는데 효과적이다. 이러한 처리 과정을 Annotation Processing이라고 하며, 대표적인 예로는 롬복(Lombok) 라이브러리, Jpa 모델 검증(Hibernate), Spring REST Docs 등이 있다.

 

표준 annotation processing API는 새로운 파일을 생성할 수 있지만 기존 파일을 수정할 수 없다는 제약조건을 갖는다. 

롬복은 annotation processing을 사용하는 대표적인 라이브러리이지만 위 제약조건이 해당하지 않는다. 어노테이션 프로세싱을 컴파일 과정에 자신을 포함시키기 위한 부트스트래핑 메커니즘으로 사용하고, 컴파일러의 내부 API를 통해 AST(추상 구문 트리)를 수정하기 때문인데, 이러한 우회방법(Hacking)은 이 글과 관련이 없으므로 다음 기회에 살펴보도록한다. 

 

 

Annotation Processing 진행 방식

어노테이션 프로세싱은 여러 라운드*로 진행된다. 각 라운드는 컴파일러가 소스 파일에서 어노테이션을 검색하고 검색된 어노테이션에 적합한 어노테이션 프로세서를 선택하는 것으로 시작된다. 컴파일러의 탐색과정이 끝난 다음 각 어노테이션 프로세서는 자신이 처리해야하는 소스(source)에 대해 차례로 호출된다.이 과정에서 파일이 생성되면, 생성된 파일을 대상으로 하는 또 다른 라운드가 시작된다. 이 프로세스는 프로세싱 단계에서 더 이상 새 파일이 생성되지 않을 때까지 계속된다.

 

어노테이션 프로세싱 API는 javax.annotation.processing 패키지에 위치해 있는데, 구현해야 할 주요 인터페이스는 Processor 인터페이스이며, 이는 AbstractProcessor 라는 추상클래스로 부분 구현되어 있다. AbstractProcessor 클래스를 상속하여 나만의 어노테이션 프로세서를 만들 수 있다.

 

 

사용방법

Getter와 Setter를 생성하는 간단한 샘플코드를 통해 annotation processor 사용방법을 익혀보도록 한다.

 

1. 프로젝트 설정 - 의존성 관리

기능 적용을 위해 아래 이미지처럼 2개의 모듈이 적용되도록 프로젝트를 생성하였다.

 

어노테이션을 정의하고 처리하는 기능을 담당하는 모듈(annotation-processor)과 커스텀 어노테이션을 사용할 모듈(anno-processing-user)이다. 우리가 일반적으로 롬봄을 사용할 때 의존성을 추가하는 구성과 동일하다고 보면 된다.

// annotation-processor 모듈 의존성
dependencies {
    // auto-service
    implementation('com.google.auto.service:auto-service:1.1.1')
    annotationProcessor('com.google.auto.service:auto-service:1.1.1')
    // type builder
    implementation("com.squareup:javapoet:1.13.0")
    ...
}

편의를 위해 추가한 라이브러리의 간단한 정보는 다음과 같다.

- auto-service: annotation processing에 필요한 메타데이터를 자동으로 생성

- javapoet: 타입 안정성에 기반한 java 소스코드 생성을 지원하는 API 제공

 

// anno-processing-user 모듈 의존성
dependencies {
    // annotation processor 모듈
    compileOnly project(':annotation-processor')
    annotationProcessor project(':annotation-processor')
    ...
}

 

 

2. 커스텀 어노테이션 정의 및 프로세서 구현

커스텀 getter와 setter를 생성하기 위해 사용할 어노테이션과 이를 처리하는 프로세서를 선언 및 구현한다. 

 

@CustomData

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

- @Target : 어노테이션이 사용될 위치를 지정하는 어노테이션이며, 여기에서는 ElementType.TYPE 을 파라미터로 받아 @CustomData가 타입에만 사용되도록 설정. (필드나 메서드 위가 아닌 클래스 유형 위에만 사용되도록 처리된다.)

 

- @Retention : 어노테이션의 유지기간을 의미하는데, SOURCE  정책을 사용하여 소스코드가 처리될 때 까지만 유지되도록 설정.

 

 

 

DataProcessor.java

이전에 선언한 @CustomData 어노테이션이 사용된 소스코드가 컴파일 될 때 처리작업을 수행할 타입이다.

추상클래스 AbstractProcessor 를 상속하고 process 메서드를 오버라이드 후 필요한 처리 로직을 메서드에 작성하면 된다.

@SupportedAnnotationTypes("com.github.jaewookmun.annotation.CustomData")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
@AutoService(Processor.class)
public class DataProcessor extends AbstractProcessor {

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

- @SupportedAnnotationTypes와 @SupportedSourceVersion을 통해 적용할 커스텀 어노테이션을 파라미터로 추가하고 지원할 JDK 버전을 지정한다. (각각의 어노테이션은 getSupportedAnnotationTypes와 getSupportedSourceVersion 메서드를 구현해서 대체할 수 있다.

- @AutoService는 auto-service 라이브러리의 기능으로 Processor 메타데이터를 자동으로 생성한다.

 

@AutoService를 사용하지 않을 경우 src/main/resources/META-INF/services/javax.annotation.processing.Processor 파일을 생성하여 해당 파일에 직접 Processor 이름(패키지 경로까지 포함)을 표기해야한다.

 

 

process 메서드의 roundEnv 파라미터에서 @CustomData 어노테이션이 사용된 자바 코드에 접근할 수 있다.

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element element : roundEnv.getElementsAnnotatedWith(CustomData.class)) {
        TypeElement classElement = (TypeElement) element;

        try {
            generateClassWithGetterAndSetter(classElement);

        } catch (IOException e) {
            // ...
        }
    }
    // ...
    
    return true;
}

 

private void generateClassWithGetterAndSetter(TypeElement classElement) throws IOException {
    // 클래스 생성
    TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className + "Supported")
                .superclass(classElement.asType())
                .addModifiers(Modifier.PUBLIC);
    // ...

    // 내부 인스턴스 필드 조회 후 getter와 setter 생성 - static 제외
    List<VariableElement> fieldList = classElement.getEnclosedElements().stream()
            .filter(e -> e.getKind() == ElementKind.FIELD)
            .map(e -> (VariableElement) e)
            .filter(v -> !v.getModifiers().contains(Modifier.STATIC) && !v.getModifiers().contains(Modifier.FINAL))
            .toList();

    for (VariableElement field : fieldList) {
        String fieldName = field.getSimpleName().toString();
        TypeName returnType = TypeName.get(field.asType());
        
        // ...

        // javapoet의 API를 활용한 java 소스코드 수정
        MethodSpec getter = MethodSpec.methodBuilder(methodName)
                .addModifiers(Modifier.PUBLIC)
                .returns(returnType)
                .addStatement("return this.$L", fieldName)
                .addJavadoc("Returns the value of $L.\n", fieldName)
                .addJavadoc("@return the $L value\n", fieldName)
                .build();

        classBuilder.addMethod(getter);

        methodName = "set" + capitalize(fieldName);
        MethodSpec setter = MethodSpec.methodBuilder(methodName)
                .addModifiers(Modifier.PUBLIC)
                .returns(TypeName.VOID)
                .addParameter(ClassName.get(field.asType()), fieldName)
                .addStatement("this.$L = $L", fieldName, fieldName)
                .addJavadoc("Sets the value of $L.\n", fieldName)
                .build();

        classBuilder.addMethod(setter);
    }

    // ...
}

 

 

 

3. 커스텀 어노테이션 사용

@CustomData
public class Person {
    private String name;
}

 

커스텀 어노테이션을 적용한 뒤 인스턴스 필드만 선언하고 프로젝트를 빌드하면 getter, setter가 추가된 클래스가 생성된다.

 

 

 

마무리하며...

어노테이션 프로세서는 단순해 보이지만 매우 강력한 도구라는 걸 느낄 수 있었다. 컴파일 타임에 코드를 생성함으로써 런타임 성능에 영향을 주지 않으면서도 반복적인 보일러플레이트 코드를 크게 줄일 수 있기 때문이다.

 

이번 예제에서는 간단한 Getter/Setter 생성을 다루었고 자주 사용되는 Lombok과 비교할 때 비효율 적으로 보이지만, 실제로는 훨씬 복잡하고 유용한 작업들을 자동화하여 생산성을 증가시킬 수 있다. 기반이 되는 코드에서 무의미하게 반복되는 작업을 자동화할 수 있는 것만으로 큰 효용을 가지고 있다 생각된다.

 

비즈니스 로직을 처리하는 코드와 달리 소스코드를 다루어 복잡해 보이고 컴파일 타임에 실행되어 디버깅이 까다롭다는 단점이 존재하지만 익숙해진다면 활용도가 높을 것이라 예상된다.

 

 

 

 

Sample Code

https://github.com/JaewookMun/programming-exercise/tree/main/java-annotation/anno-processing-user

 

programming-exercise/java-annotation/anno-processing-user at main · JaewookMun/programming-exercise

practice framework or skill such as spring, jpa, and so on - JaewookMun/programming-exercise

github.com

 

 

 

References

- Baeldung : https://www.baeldung.com/java-annotation-processing-builder

- Baeldung : https://www.baeldung.com/java-poet

- post : https://hannesdorfmann.com/annotation-processing/annotationprocessing101/