1. 애노테이션 프로세서 소개
- 애노테이션 프로세서는 자바 컴파일 시점에 특정 애노테이션이 붙은 요소를 검색하고 처리하는 특별한 도구입니다.
- 컴파일 시점에 동작하기 때문에 런타임 오버헤드가 없으며, 소스 코드나 바이트 코드를 분석하고 생성할 수 있습니다.
- 프로젝트 빌드 과정에서 자동으로 코드를 생성하거나 검증하는 데 주로 사용됩니다.
1.1 주요 특징과 장점
- 컴파일 타임 검증: 애노테이션 프로세서는 컴파일 단계에서 오류를 발견하여 런타임 오류를 미리 방지합니다.
- 보일러플레이트 코드 자동 생성: 반복적이고 기계적인 코드를 자동으로 생성하여 개발자 생산성을 높입니다.
- 메타프로그래밍 지원: 코드가 코드를 생성하는 메타프로그래밍 패러다임을 자바에서 구현할 수 있게 합니다.
- 타입 안전성 향상: 문자열 기반 참조 대신 컴파일러가 검증할 수 있는 형태로 코드를 생성합니다.
1.2 대표적인 활용 사례
- Lombok:
@Getter
,@Setter
,@Builder
등의 애노테이션으로 반복적인 코드 자동 생성 - MapStruct: 객체 간 매핑을 위한 코드 자동 생성
- Dagger/Hilt: 의존성 주입을 위한 팩토리 코드 자동 생성
- QueryDSL: 타입 안전한 쿼리를 위한 Q 클래스 생성
- JPA 메타모델 생성기: JPA 정적 메타모델 클래스 생성
2. 애노테이션 프로세서 작동 원리
- 애노테이션 프로세서는 Java Compiler API(JSR 269)를 기반으로 작동합니다.
- javac 컴파일러가 소스 코드를 처리할 때 특정 라운드(round)에 걸쳐 애노테이션 프로세싱이 이루어집니다.
2.1 처리 과정
- 컴파일러는 소스 코드를 파싱하여 추상 구문 트리(AST)를 생성합니다.
- 컴파일러는 각 라운드마다 아직 처리되지 않은 애노테이션을 찾아 등록된 프로세서를 실행합니다.
- 프로세서는 애노테이션이 달린 요소(클래스, 메서드, 필드 등)에 접근하여 정보를 수집합니다.
- 필요에 따라 새로운 소스 파일을 생성하거나 오류/경고 메시지를 출력합니다.
- 새로운 소스 파일이 생성되면 다음 라운드에서 해당 파일도 처리 대상이 됩니다.
- 더 이상 처리할 애노테이션이 없거나 새로운 소스 파일이 생성되지 않으면 프로세싱이 종료됩니다.
2.2 주요 컴포넌트
javax.annotation.processing.Processor
인터페이스: 모든 애노테이션 프로세서의 기본 인터페이스javax.annotation.processing.AbstractProcessor
클래스: 프로세서 구현을 위한 편의 클래스javax.lang.model
패키지: 언어 모델 요소 및 타입에 접근하기 위한 APIjavax.annotation.processing.Filer
인터페이스: 새로운 소스/리소스 파일 생성을 위한 APIjavax.tools.Diagnostic
클래스: 컴파일러 메시지(오류/경고) 출력을 위한 API
정보
Java 9부터는 javax.annotation.processing
패키지가 jakarta.annotation.processing
으로 이동했으며,
모듈 시스템 도입에 따라 module-info.java
에서 프로세서 사용을 위한 설정이 필요할 수 있습니다.
3. 간단한 애노테이션 프로세서 구현하기
3.1 개발 환경 설정
- 애노테이션 프로세서는 독립적인 모듈로 개발하는 것이 좋습니다.
- Maven이나 Gradle을 사용하여 프로젝트를 구성합니다.
Gradle 의존성 설정 예시
dependencies {
implementation 'com.google.auto.service:auto-service-annotations:1.0'
annotationProcessor 'com.google.auto.service:auto-service:1.0'
// 자바 코드 생성을 위한 유틸리티 라이브러리
implementation 'com.squareup:javapoet:1.13.0'
}
3.2 간단한 애노테이션 정의
- 먼저 프로세싱할 대상 애노테이션을 정의합니다.
package com.example.processor;
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 GenerateToString {
}
@Target
: 애노테이션이 적용될 요소를 지정 (여기서는 클래스, 인터페이스 등)@Retention
: 애노테이션이 유지되는 범위 지정 (SOURCE는 컴파일 시에만 유지)
3.3 프로세서 클래스 구현
package com.example.processor;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.example.processor.GenerateToString")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class ToStringProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// GenerateToString 애노테이션이 붙은 요소들 가져오기
for (Element element : roundEnv.getElementsAnnotatedWith(GenerateToString.class)) {
// 클래스인지 확인
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"GenerateToString은 클래스에만 적용할 수 있습니다.",
element
);
continue;
}
TypeElement typeElement = (TypeElement) element;
generateToStringMethod(typeElement);
}
return true;
}
private void generateToStringMethod(TypeElement typeElement) {
// 클래스의 모든 필드 가져오기
List<VariableElement> fields = typeElement.getEnclosedElements().stream()
.filter(e -> e.getKind() == ElementKind.FIELD)
.map(e -> (VariableElement) e)
.collect(Collectors.toList());
// toString 메소드 생성 로직 구현
// JavaPoet 라이브러리를 사용한 코드 생성
// ...
}
}
@AutoService(Processor.class)
: 서비스 프로바이더 설정 자동화 (META-INF/services 파일 생성)@SupportedAnnotationTypes
: 이 프로세서가 처리할 애노테이션 타입 지정@SupportedSourceVersion
: 지원하는 자바 소스 버전 지정
4. 코드 생성 기법
4.1 JavaPoet 라이브러리 활용
- JavaPoet은 자바 소스 코드 생성을 위한 편리한 API를 제공합니다.
- 클래스, 메서드, 필드 등의 코드를 프로그래매틱하게 생성할 수 있습니다.
private void generateToStringMethod(TypeElement typeElement) {
String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();
String className = typeElement.getSimpleName().toString();
// 필드 목록 가져오기
List<? extends Element> allMembers = processingEnv.getElementUtils().getAllMembers(typeElement);
List<VariableElement> fields = allMembers.stream()
.filter(member -> member.getKind() == ElementKind.FIELD)
.filter(field -> !field.getModifiers().contains(Modifier.STATIC))
.map(member -> (VariableElement) member)
.collect(Collectors.toList());
// toString 메서드 생성
MethodSpec.Builder toStringMethodBuilder = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class);
// 메서드 내용 구성
StringBuilder sb = new StringBuilder();
sb.append("return \"").append(className).append("{\" +\n");
for (int i = 0; i < fields.size(); i++) {
VariableElement field = fields.get(i);
String fieldName = field.getSimpleName().toString();
sb.append(" \"").append(fieldName).append("=\" + ").append(fieldName);
if (i < fields.size() - 1) {
sb.append(" + \", \" +\n");
} else {
sb.append(" + \"}\"");
}
}
toStringMethodBuilder.addStatement(sb.toString());
// 클래스 생성
TypeSpec generatedClass = TypeSpec.classBuilder(className + "Generated")
.superclass(typeElement.asType())
.addModifiers(Modifier.PUBLIC)
.addMethod(toStringMethodBuilder.build())
.build();
// 파일 생성
try {
JavaFile.builder(packageName, generatedClass)
.build()
.writeTo(processingEnv.getFiler());
} catch (IOException e) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"코드 생성 중 오류 발생: " + e.getMessage()
);
}
}
4.2 템플릿 엔진 활용
- FreeMarker, Velocity 등의 템플릿 엔진을 사용하여 코드를 생성할 수도 있습니다.
- 복잡한 형태의 코드를 생성할 때 더 직관적일 수 있습니다.
// FreeMarker 템플릿 엔진 설정 예시
private void generateFromTemplate(TypeElement element) {
try {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
cfg.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "templates");
cfg.setDefaultEncoding("UTF-8");
Template template = cfg.getTemplate("tostring.ftl");
// 데이터 모델 구성
Map<String, Object> model = new HashMap<>();
model.put("className", element.getSimpleName().toString());
model.put("packageName", processingEnv.getElementUtils().getPackageOf(element).toString());
// ... 필드 정보 등 추가
// 파일 생성
String className = element.getSimpleName() + "Generated";
JavaFileObject file = processingEnv.getFiler().createSourceFile(
element.getQualifiedName() + "Generated"
);
try (Writer writer = file.openWriter()) {
template.process(model, writer);
}
} catch (Exception e) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
"템플릿 처리 중 오류 발생: " + e.getMessage()
);
}
}