@Getter
@Setter
@NoArgsConstructor
public class Car {
private String name;
private int price;
}
Java 개발자라면 누구나 한번쯤 접해보았을 보일러 플레이트 코드
정리의 끝판왕 Lombok
입니다.
Spring 의존성을 주입할때도 @RequiredArgsConstructor
를 활용해 참 편하게 주입받아 왔는데요.
그렇다면 Lombok
은 애너테이션을 붙이면 어떻게 작성하지도 않은 코드를 만들어주게 되는 것일까요?
Lombok의 동작을 이해하기 위해서는 두가지 개념에 대한 이해가 필요합니다.
첫 번째는 AST(Abstract Syntax Tree)
, 두 번째는 Annotation Processor
입니다.
Annotation Processor
는 소스 코드에 포함된 애너테이션을 처리하고 이를 기반으로 추가적인 작업을 수행하는 도구입니다.
Java 컴파일러에 의해 호출
되며, 컴파일 시간에 코드를 생성
하거나 수정
하는 등의 작업을 수행할 수 있습니다.
AST는 추상 구문 트리
라고도 부르며, Java에만 국한된 개념이 아닌 컴파일러
가 필요한 프로그래밍 언어
라면 어디서든 들어볼 수 있는 용어입니다.
AST는 컴파일 과정 중 구분 분석
의 결과물로, 개발자가 작성한 Java 코드를 파싱
하여 만들어집니다.
AST가 추상 구문 트리라고 불리는 이유는 구체적인 코드의 세부사항
이나 문법적 세부사항
은 나타내지 않기 때문입니다.
public class Car {
private String name;
private int price;
}
이런 간단한 클래스가 있다고 해보겠습니다.
이 클래스의 소스코드
를 파싱하여 AST
를 생성하면 어떤 형태가 나올까요?
손으로 그리니 알아보기가 힘드네요.. 똥손 사과드립니다.
가장 상위 노드는 클래스 선언(Class Declaration)
이 됩니다.
클래스 선언부의 아래에는 접근제어 수준
을 나타내는 노드, 클래스의 식별자
(이름), Body
(내용)이 있습니다.
Body에는 현재 Car 클래스가 필드만 있어서 Field Declartion
만 표현했지만 메서드를 가지고 있다면
Method Declartion
도 Body에 연결되게 됩니다.
두 개의 필드를 가지고 있지만, 간단하게 그림을 그리기 위해 String
타입의 name
만 표현했습니다.
그렇다면 왜 Lombok
의 동작을 이해하려면 AST
와 Annotation Processor
를 이해해야 할까요?
Lombok
의 애너테이션을 소스코드에 붙이게 되면, 소스코드
를 파싱
하고 AST
를 만든 뒤에 붙인 애너테이션에 따라서 Annotation Processor
가 동작하게 되면서 기존 AST
를 수정
하게 됩니다.
수정된 AST
를 이용해 바이트 코드
를 만들게 되고, 생성된 class 파일
을 디컴파일
해보면 내가 작성하지 않은 메서드
가 들어가 있는 것을 볼 수 있습니다.
Car 클래스에 메서드가 없었으나, @Getter
또는 @Setter
를 이용하여 Method Declaration
을 생성해서 기존 AST
의 Body
에 삽입하게 되는 것입니다.
먼저 프로젝트는 gradle multi-module
방식으로 구성했습니다.
가장 중요한 annotation
프로젝트부터 기본 셋팅을 하겠습니다.
plugins {
id 'java'
}
// JDK 11 버전을 사용
sourceCompatibility = 11
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.apache.commons:commons-lang3:3.12.0'
testImplementation 'junit:junit:4.13.1'
compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
}
test {
useJUnitPlatform()
}
// com.sun.tools.javac 패키지는 현재 접근이 불가능한 상태이므로, --add-exports 옵션을 줘야한다.
compileJava {
doFirst {
options.compilerArgs = [
'--add-exports', 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED',
'--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
'--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
'--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED'
]
}
}
제가 작업한 JDK 11
버전 기준으로는 com.sun.tools.javac
패키지는 외부 접근
이 불가한 상태입니다.
따라서 컴파일 옵션
을 줘서 빌드해야 문제가 생기지 않습니다.
Lombok
이 사용하는 패키지의 클래스들은 공개 목적으로 만들어진 API가 아니였습니다.
따라서 AST
를 조작하는 것은 사실상 해킹
이라고 합니다.
하위 호환성
을 중요하게 여겨서 결함조차 잡지 않는 Java 진영에서 황급히 이 패키지를 접근 불가
하게 처리한 것도 이런 이유에서 비롯된 것이 아닐까 생각합니다.
총 3가지 기능
을 만들어보겠습니다.
@Getter
를 만들기 전에, 모든 Processor에서 사용하는 TreeModifier
클래스를 작성하겠습니다.
public class TreeModifier {
private Trees trees;
private Context context;
private TreeMaker treeMaker;
private Names names;
private TreePathScanner<Object, CompilationUnitTree> scanner;
public TreeModifier(ProcessingEnvironment processingEnvironment) {
final JavacProcessingEnvironment javacProcessingEnvironment = (JavacProcessingEnvironment) processingEnvironment;
this.trees = Trees.instance(processingEnvironment);
this.context = javacProcessingEnvironment.getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
public void setClassDefModifyStrategy(Consumer<JCTree.JCClassDecl> strategy) {
this.scanner = new TreePathScanner<>() {
@Override
public Trees visitClass(ClassTree node, CompilationUnitTree compilationUnitTree) {
JCTree.JCCompilationUnit compilationUnit = (JCTree.JCCompilationUnit) compilationUnitTree;
if (compilationUnit.sourcefile.getKind() == JavaFileObject.Kind.SOURCE) {
compilationUnit.accept(new TreeTranslator() {
/*
* AST 클래스 노드를 순회하며 AST를 재정의 한다.
* -> 컴파일 타임에 추상 구문 트리(Abstract Syntax Tree)를 조작(원하는 메서드 삽입)
* */
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
super.visitClassDef(jcClassDecl);
/*
* AST 수정이 이루어지는 부분이다.
* AST 수정 전략은 외부에서 주입 받아 사용할 수 있다.
* */
strategy.accept(jcClassDecl);
}
});
}
return trees;
}
};
}
/*
* AST를 순회하면서 AST를 조작한다.
* -> 원하는 메서드를 실제 삽입하는 부분이다.
* */
public void modifyTree(Element element) {
if (Objects.nonNull(scanner)) {
final TreePath path = trees.getPath(element);
scanner.scan(path, path.getCompilationUnit());
}
}
public TreeMaker getTreeMaker() {
return treeMaker;
}
public Names getNames() {
return names;
}
}
TreeModifier
클래스는 AST 수정 전략
을 주입받아서, AST
를 조작합니다.
modifyTree
메서드가 실질적으로 AST
를 조작하는 부분입니다.
이제 @Getter
를 붙이면 모든 필드에 대해 Getter
가 생성되도록 GetterProcessor
클래스를 작성해보겠습니다.
/*
* Annotation Processor를 쉽게 등록하려는 목적으로 @AutoService를 사용한다.
* com.rex.annotation 패키지의 Getter 애너테이션을 지원한다.
* JDK 11 버전을 지원한다.
* */
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.rex.annotation.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class GetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// TreeModifier의 객체를 만들어준다.
TreeModifier treeModifier = new TreeModifier(processingEnv);
// AST 조작에 필요한 전략을 셋팅해준다.
treeModifier.setClassDefModifyStrategy(getAppendGetterStrategy(treeModifier));
// @Getter 애너테이션이 붙은 모든 타입(클래스, 열거타입, 인터페이스)들을 순회한다.
for (Element element : roundEnv.getElementsAnnotatedWith(Getter.class)) {
// @Getter 애너테이션이 클래스가 아닌 곳에 있는 경우, 에러 메시지를 빌드 로그에 남겨준다.
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotation Not Supported");
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing >> " + element.getSimpleName().toString());
// @Getter 애너테이션이 클래스에 붙어 있는 경우, 모든 필드에 대해 Getter를 생성해주는 메서드를 AST 트리 조작으로 삽입한다.
treeModifier.modifyTree(element);
}
}
return true;
}
private Consumer<JCTree.JCClassDecl> getAppendGetterStrategy(TreeModifier treeModifier) {
TreeMaker treeMaker = treeModifier.getTreeMaker();
Names names = treeModifier.getNames();
/*
* AST 조작 전략은 Consumer로 제공한다.
* -> Consumer로 제공시 이 전략을 사용하는 곳에서 accept만 호출하면 설정된 전략대로 AST 조작이 이루어진다.
*
* */
return jcClassDecl -> {
List<JCTree> members = jcClassDecl.getMembers();
for (JCTree member : members) {
/*
* 모든 멤버를 순회하면서, 멤버가 변수(Field)일 경우에만 Getter를 만들어준다.
* AST에 삽입할 메서드는 JCTree.JCMethodDecl 타입이며, append 메서드를 활용해 삽입할 메서드를 붙여준다.
* */
if (member instanceof JCTree.JCVariableDecl) {
JCTree.JCMethodDecl getter = createGetter(treeMaker, names, (JCTree.JCVariableDecl) member);
jcClassDecl.defs = jcClassDecl.defs.append(getter);
}
}
};
}
private JCTree.JCMethodDecl createGetter(TreeMaker treeMaker, Names names, JCTree.JCVariableDecl member) {
// 필드의 이름을 문자열로 반환받는다.
String memberName = member.name.toString();
// 메서드의 명명 규칙은 get + 필드 이름의 첫 글자를 대문자로 + 나머지 문자
String methodName = "get" + memberName.substring(0, 1).toUpperCase() +
memberName.substring(1);
// 메서드가 잘 생성되고 있는지 빌드 로그로 확인할 수 있다.
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, methodName);
return treeMaker.MethodDef(
treeMaker.Modifiers(1), // flag 번호에 따라 접근 제어나 추상 메서드 등 다양한 제어를 할 수 있다. 1번은 public 이다.
names.fromString(methodName), // 메서드의 이름은 아까 지어놓은 대로 getXXXX 과 같다.
(JCTree.JCExpression) member.getType(), // 대상 필드의 타입 그대로 반환하도록 타입을 지정해준다.
List.nil(), // 필요하지 않은 부분이라도 List를 인수로 받고 있기 때문에 빈 리스트를 넣어준다.
List.nil(),
List.nil(),
/*
* 메서드의 본문에 해당하는 부분이다.
* Block의 flag는 들여쓰기 수준을 말한다.
* Ident 메서드는 AST 노드 중 하나로 식별자를 나타낸다. 이렇게 반들어진 식별자는 필드 변수를 나타내는 AST 구조 표현에 사용된다.
* */
treeMaker.Block(1,
List.of(treeMaker.Return(treeMaker.Ident(member.getName())))
),
null
);
}
}
주석은 상세히 달아놓았지만, 한 부분씩 뜯어보겠습니다.
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.rex.annotation.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@AutoService
를 이용하면 서비스 제공자 클래스를 간편하게 등록할 수 있는 기능을 제공합니다.
이렇게 하지 않으면 매번 서비스 제공자 클래스
등록 코드를 작성
해야 하기 때문에 코드의 중복
도 발생할 수 있습니다.
@SupportedAnnotationTypes
는 어떤 애너테이션에 대해 이 프로세서가 동작할 것인지 정의합니다.
즉, 이 프로세서가 지원
할 애너테이션
의 타입
을 지정하는 부분입니다.
@SupportedSourceVersion
은 지원하려는 JDK 버전을 명시하는 부분입니다.
현재 저는 JDK 11
버전을 지원하도록 작성했으므로 11 이외의 버전에서는 동작하지 않습니다.
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// TreeModifier의 객체를 만들어준다.
TreeModifier treeModifier = new TreeModifier(processingEnv);
// AST 조작에 필요한 전략을 셋팅해준다.
treeModifier.setClassDefModifyStrategy(getAppendGetterStrategy(treeModifier));
// @Getter 애너테이션이 붙은 모든 타입(클래스, 열거타입, 인터페이스)들을 순회한다.
for (Element element : roundEnv.getElementsAnnotatedWith(Getter.class)) {
// @Getter 애너테이션이 클래스가 아닌 곳에 있는 경우, 에러 메시지를 빌드 로그에 남겨준다.
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotation Not Supported");
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing >> " + element.getSimpleName().toString());
// @Getter 애너테이션이 클래스에 붙어 있는 경우, 모든 필드에 대해 Getter를 생성해주는 메서드를 AST 트리 조작으로 삽입한다.
treeModifier.modifyTree(element);
}
}
return true;
}
이 부분이 AST
를 조작하는 핵심 메서드
입니다.
이 메서드 에서는TreeModifer
의 객체를 생성하고, 조작에 필요한 전략
을 셋팅
해줍니다.
@Getter
애너테이션이 붙은 모든 클래스를 순회하면서, modifyTree
메서드를 호출해 AST
를 조작
합니다.
마지막에 true
를 return
하는 이유는 무엇일까요?
그 이유는 javax.annotation.processing.AbstractProcessor
클래스의 규약 때문입니다.
Annotation Processor
가 처리할 요소가 없거나 모든 요소에 대한 처리가 완료
되었음을 알려주는 표시입니다.Annotation Processors
는 아직 처리해야 할 요소가 남아있다는 표시입니다. 다음 라운드를 진행하지 말아야 하며, 더 많은 작업
을 해야한다는 것을 의미합니다.private Consumer<JCTree.JCClassDecl> getAppendGetterStrategy(TreeModifier treeModifier) {
TreeMaker treeMaker = treeModifier.getTreeMaker();
Names names = treeModifier.getNames();
/*
* AST 조작 전략은 Consumer로 제공한다.
* -> Consumer로 제공시 이 전략을 사용하는 곳에서 accept만 호출하면 설정된 전략대로 AST 조작이 이루어진다.
*
* */
return jcClassDecl -> {
List<JCTree> members = jcClassDecl.getMembers();
for (JCTree member : members) {
/*
* 모든 멤버를 순회하면서, 멤버가 변수(Field)일 경우에만 Getter를 만들어준다.
* AST에 삽입할 메서드는 JCTree.JCMethodDecl 타입이며, append 메서드를 활용해 삽입할 메서드를 붙여준다.
* */
if (member instanceof JCTree.JCVariableDecl) {
JCTree.JCMethodDecl getter = createGetter(treeMaker, names, (JCTree.JCVariableDecl) member);
jcClassDecl.defs = jcClassDecl.defs.append(getter);
}
}
};
}
이 부분은 Getter
를 생성하는 AST 수정 전략
을 얻어오는 메서드
입니다.
클래스가 가진 모든 멤버(필드, 메서드)
를 순회하면서 찾은 멤버가 필드
일때만 Getter를 추가
하도록 jcClassDecl의 defs에 append
해줍니다.
Getter
를 추가하려면 append
와 prepend
두 가지가 사용가능 한데, append
를 사용하면 소스코드의 뒤
쪽에 붙여지고 prepend
를 사용하면 소스코드의 앞
에 붙여지는 것 이외에는 큰 변화가 없습니다.
private JCTree.JCMethodDecl createGetter(TreeMaker treeMaker, Names names, JCTree.JCVariableDecl member) {
// 필드의 이름을 문자열로 반환받는다.
String memberName = member.name.toString();
// 메서드의 명명 규칙은 get + 필드 이름의 첫 글자를 대문자로 + 나머지 문자
String methodName = "get" + memberName.substring(0, 1).toUpperCase() +
memberName.substring(1);
// 메서드가 잘 생성되고 있는지 빌드 로그로 확인할 수 있다.
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, methodName);
return treeMaker.MethodDef(
treeMaker.Modifiers(1), // flag 번호에 따라 접근 제어나 추상 메서드 등 다양한 제어를 할 수 있다. 1번은 public 이다.
names.fromString(methodName), // 메서드의 이름은 아까 지어놓은 대로 getXXXX 과 같다.
(JCTree.JCExpression) member.getType(), // 대상 필드의 타입 그대로 반환하도록 타입을 지정해준다.
List.nil(), // 필요하지 않은 부분이라도 List를 인수로 받고 있기 때문에 빈 리스트를 넣어준다.
List.nil(),
List.nil(),
/*
* 메서드의 본문에 해당하는 부분이다.
* Block의 flag는 들여쓰기 수준을 말한다.
* Ident 메서드는 AST 노드 중 하나로 식별자를 나타낸다. 이렇게 반들어진 식별자는 필드 변수를 나타내는 AST 구조 표현에 사용된다.
* */
treeMaker.Block(1,
List.of(treeMaker.Return(treeMaker.Ident(member.getName())))
),
null
);
}
이 부분이 실제 Getter
의 메서드를 만들어내는 부분입니다.
필드의 이름
과 get
키워드를 혼합해서 메서드의 이름을 만들어내고, 중간에 이 메서드가 잘 호출됬는지 확인합니다.
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, methodName);
println
을 사용하지 않고, 이렇게 메시지를 확인하는 이유는 컴파일 타임
에 발생하는 로그이기 때문에 println
을 사용하면 아무것도 확인할 수 없기 때문입니다.
각각의 내용은 주석으로 달아 두었으니 설명은 생략
하겠습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
}
간단하게 애너테이션
을 먼저 정의
해줍니다.
컴파일타임에만 이 애너테이션의 정보가 필요하기 때문에 RetentionPolicy
는 SOURCE
입니다.
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.rex.annotation.Setter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class SetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// TreeModifier의 객체를 만들어준다.
TreeModifier treeModifier = new TreeModifier(processingEnv);
// AST 조작에 필요한 전략을 셋팅해준다.
treeModifier.setClassDefModifyStrategy(getAppendSetterStrategy(treeModifier));
// @Setter 애너테이션이 붙은 모든 타입(클래스, 열거타입, 인터페이스)들을 순회한다.
for (Element element : roundEnv.getElementsAnnotatedWith(Setter.class)) {
if (element.getKind() != ElementKind.CLASS) {
// @Setter 애너테이션이 클래스가 아닌 곳에 있는 경우, 에러 메시지를 빌드 로그에 남겨준다.
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotation Not Supported");
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing >> " + element.getSimpleName().toString());
// @Setter 애너테이션이 클래스에 붙어 있는 경우, 모든 필드에 대해 Setter를 생성해주는 메서드를 AST 트리 조작으로 삽입한다.
treeModifier.modifyTree(element);
}
}
return true;
}
private Consumer<JCTree.JCClassDecl> getAppendSetterStrategy(TreeModifier treeModifier) {
TreeMaker treeMaker = treeModifier.getTreeMaker();
Names names = treeModifier.getNames();
/*
* AST 조작 전략은 Consumer로 제공한다.
* -> Consumer로 제공시 이 전략을 사용하는 곳에서 accept만 호출하면 설정된 전략대로 AST 조작이 이루어진다.
*
* */
return jcClassDecl -> {
List<JCTree> members = jcClassDecl.getMembers();
for (JCTree member : members) {
/*
* 모든 멤버를 순회하면서, 멤버가 변수(Field)일 경우에만 Setter를 만들어준다.
* AST에 삽입할 메서드는 JCTree.JCMethodDecl 타입이며, append 메서드를 활용해 삽입할 메서드를 붙여준다.
* */
if (member instanceof JCTree.JCVariableDecl) {
JCTree.JCMethodDecl setter = createSetter(treeMaker, names, (JCTree.JCVariableDecl) member);
jcClassDecl.defs = jcClassDecl.defs.append(setter);
}
}
};
}
private JCTree.JCMethodDecl createSetter(TreeMaker treeMaker, Names names, JCTree.JCVariableDecl member) {
// 필드의 이름을 문자열로 반환받는다.
String memberName = member.name.toString();
// 메서드의 명명 규칙은 set + 필드 이름의 첫 글자를 대문자로 + 나머지 문자
String methodName = "set" + memberName.substring(0, 1).toUpperCase() +
memberName.substring(1);
// 메서드가 잘 생성되고 있는지 빌드 로그를 통해 확인할 수 있다.
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, methodName);
/*
* 파라미터가 있는 메서드이기 때문에, 필요한 파라미터를 설정해주어야 한다.
* 파라미터의 이름, 파라미터의 타입을 넣어주어야 한다.
* owner는 사용하지 않는 인수이므로 null을 넣어준다.
* */
JCTree.JCVariableDecl param = treeMaker.Param(names.fromString("_" + memberName), member.vartype.type, null);
return treeMaker.MethodDef(
treeMaker.Modifiers(1), // public 접근 제어자를 가진다.
names.fromString(methodName), // 메서드의 이름은 setXXXX와 같은 규칙으로 생성된다.
treeMaker.TypeIdent(TypeTag.VOID), // 반환 타입이 없으므로, VOID를 지정해준다.
List.nil(), // 필요 없는 인수는 List.nil() 로 emptyList를 넣어준다.
List.of(param), // Getter와 다르게 파라미터가 있으므로, 파라미터를 넣어준다.
List.nil(),
/*
* 들여쓰기 레벨은 1로 지정한다.
* treeMaker.Exec는 AST에서 실행문(Statement)를 나타내는 노드를 생성한다.
* treeMaker.Assign은 AST에서 대입문(Assignment)을 나타내는 노드를 생성한다.
* treeMaker.Ident는 식별자를 생성하는 역할을 한다.
* 즉, member라는 이름을 가진 변수에, param.name 이름으로 들어오는 파라미터의 값을 대입하는 대입문을
* 실행하라 라는 의미다.
* */
treeMaker.Block(1,
List.of(treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(member), treeMaker.Ident(param.name))))
),
null
);
}
}
코드는 Getter
를 생성하는 부분과 대부분 일치
합니다.
다른 부분은 createSetter
부분인데 이 부분은 설명드리고 넘어가겠습니다.
private JCTree.JCMethodDecl createSetter(TreeMaker treeMaker, Names names, JCTree.JCVariableDecl member) {
// 필드의 이름을 문자열로 반환받는다.
String memberName = member.name.toString();
// 메서드의 명명 규칙은 set + 필드 이름의 첫 글자를 대문자로 + 나머지 문자
String methodName = "set" + memberName.substring(0, 1).toUpperCase() +
memberName.substring(1);
// 메서드가 잘 생성되고 있는지 빌드 로그를 통해 확인할 수 있다.
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, methodName);
/*
* 파라미터가 있는 메서드이기 때문에, 필요한 파라미터를 설정해주어야 한다.
* 파라미터의 이름, 파라미터의 타입을 넣어주어야 한다.
* owner는 사용하지 않는 인수이므로 null을 넣어준다.
* */
JCTree.JCVariableDecl param = treeMaker.Param(names.fromString("_" + memberName), member.vartype.type, null);
return treeMaker.MethodDef(
treeMaker.Modifiers(1), // public 접근 제어자를 가진다.
names.fromString(methodName), // 메서드의 이름은 setXXXX와 같은 규칙으로 생성된다.
treeMaker.TypeIdent(TypeTag.VOID), // 반환 타입이 없으므로, VOID를 지정해준다.
List.nil(), // 필요 없는 인수는 List.nil() 로 emptyList를 넣어준다.
List.of(param), // Getter와 다르게 파라미터가 있으므로, 파라미터를 넣어준다.
List.nil(),
/*
* 들여쓰기 레벨은 1로 지정한다.
* treeMaker.Exec는 AST에서 실행문(Statement)를 나타내는 노드를 생성한다.
* treeMaker.Assign은 AST에서 대입문(Assignment)을 나타내는 노드를 생성한다.
* treeMaker.Ident는 식별자를 생성하는 역할을 한다.
* 즉, member라는 이름을 가진 변수에, param.name 이름으로 들어오는 파라미터의 값을 대입하는 대입문을
* 실행하라 라는 의미다.
* */
treeMaker.Block(1,
List.of(treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(member), treeMaker.Ident(param.name))))
),
null
);
}
메서드의 이름을 만드는 부분까지는 동일합니다.
여기서 다른 점은 파라미터
를 받아야 한다는 점입니다. JCTree.JCVariableDecl
타입의 파라미터 객체를 만듭니다.
여기서 주의할점은, 멤버의 이름(필드명) 앞에 언더 하이픈(_)또는 구분할 수 있는 문자
를 넣어주어야 한다는 것입니다.
만약 넣어주지 않고 멤버의 이름을 그대로 사용한다면 메서드는 생성
되지만 대입문이 없는 메서드
가 나오게 됩니다.
아까와 달라진 부분은, 파라미터가 생겼다는 것
이고 반환타입이 없기 때문에 VOID형의 ENUM을 넣어주었다는 점
입니다.
treeMaker
의 Block 메서드
는 본문을 만들어주는 부분입니다.
여러 메서드가 중첩되어 있는데 주석과 같이 풀어보면 간단합니다.
treeMaker.Exec
는 실행문을 나타내는 노드를 생성합니다.
이 메서드를 호출하면서 인자로 treeMaker.Assign
이 반환한 값을 인자로 제공합니다. treeMaker.Assign
은 대입문을 나타내는 노드를 생성하는 역할을 해줍니다.
treeMaker.Ident는 식별자를 생성하는 역할을 하는데,
일련의 과정들을 종합해서 풀어보면, member
라는 이름을 가진 변수에 _member
를 대입하라는 뜻입니다.
그래서 나중에 직접 @Setter
를 사용해서 바이트코드를 디컴파일
해보면 이런 결과가 나오게 되는 것입니다.
JPA
를 사용하면서 기본 생성자
를 만들때 참 애용했던 친구였는데 이렇게 만들어보니 새로웠습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NoArgsConstructor {
AccessLevel accessLevel() default AccessLevel.PUBLIC;
}
public enum AccessLevel {
/*
* 기본 생성자 생성시 접근 제어 수준을 지정할 수 있는 열거 타입
* PUBLIC -> public
* PROTECTED -> protected
* PACKAGE -> default
* PRIVATE -> private
* */
PUBLIC, PROTECTED, PRIVATE, PACKAGE
}
애너테이션은 AccessLevel
을 포함하고 있고, 이 값에 따라서 접근제어 레벨
이 다른 생성자
를 만들어주게 됩니다.
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.rex.annotation.NoArgsConstructor")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class NoArgsProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
TreeModifier treeModifier = new TreeModifier(processingEnv);
for (Element element : roundEnv.getElementsAnnotatedWith(NoArgsConstructor.class)) {
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Annotation Not Supported");
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing >> " + element.getSimpleName().toString());
// AccessLevel에 따라 생성자의 접근 제어 레벨이 변동되기 때문에 애너테이션에 접근해서 값을 뽑아온다.
AccessLevel accessLevel = element.getAnnotation(NoArgsConstructor.class).accessLevel();
// AST 조작 전략을 셋팅해준다.
treeModifier.setClassDefModifyStrategy(getAppendNoArgsStrategy(treeModifier, accessLevel));
treeModifier.modifyTree(element);
}
}
return true;
}
private Consumer<JCTree.JCClassDecl> getAppendNoArgsStrategy(TreeModifier treeModifier, AccessLevel accessLevel) {
TreeMaker treeMaker = treeModifier.getTreeMaker();
Names names = treeModifier.getNames();
return jcClassDecl -> {
ListBuffer<JCTree> newMembers = new ListBuffer<>();
for (JCTree member : jcClassDecl.defs) {
/*
* 컴파일러는 기본적으로 파라미터 있는 생성자가 없다면, 파라미터가 없는 public 생성자를 넣어주게 된다.
* 이럴 경우 생성자가 중복 정의되기 때문에 원하는 접근 제어 레벨의 생성자를 삽입할때 에러가 발생한다.
* 따라서 이미 매개변수 없는 생성자가 있다면 제외해야 한다.
* */
if (member instanceof JCTree.JCMethodDecl) {
// 메서드가 생성자인지 확인한다.
boolean isConstructor = ((JCTree.JCMethodDecl) member).name.toString().equals("<init>");
// 생성자이면서 파라미터가 있다면 다시 넣어준다.
if (isConstructor && !((JCTree.JCMethodDecl) member).getParameters().isEmpty()) {
newMembers.add(member);
} else if (!isConstructor) {
// 생성자가 아니라면 메서드이므로 추가한다.
newMembers.append(member);
}
} else {
// 메서드가 아닌 멤버는 전부 넣어준다.
newMembers.append(member);
}
}
jcClassDecl.defs = newMembers.toList();
JCTree.JCMethodDecl noArgs = createNoArgs(treeMaker, names, accessLevel);
jcClassDecl.defs = jcClassDecl.defs.append(noArgs);
};
}
private JCTree.JCMethodDecl createNoArgs(TreeMaker treeMaker, Names names, AccessLevel accessLevel) {
long flag = 0L;
switch (accessLevel) {
case PUBLIC:
flag = 1L;
break;
case PACKAGE:
flag = 0L;
break;
case PRIVATE:
flag = 2L;
break;
case PROTECTED:
flag = 4L;
break;
}
return treeMaker.MethodDef(
treeMaker.Modifiers(flag),
names.init,
treeMaker.TypeIdent(TypeTag.VOID),
List.nil(),
List.nil(),
List.nil(),
treeMaker.Block(0, List.nil()),
null
);
}
}
이 부분은 @Getter & @Setter
와 상이한 부분이 많아서 뜯어보겠습니다.
// AccessLevel에 따라 생성자의 접근 제어 레벨이 변동되기 때문에 애너테이션에 접근해서 값을 뽑아온다.
AccessLevel accessLevel = element.getAnnotation(NoArgsConstructor.class).accessLevel();
// AST 조작 전략을 셋팅해준다.
treeModifier.setClassDefModifyStrategy(getAppendNoArgsStrategy(treeModifier, accessLevel));
treeModifier.modifyTree(element);
애너테이션의 accessLevel
값을 이용해서 AST 조작
을 해야하기 때문에 뽑아서 전략을 생성하는 메서드에 넘겨줘야 합니다.
private Consumer<JCTree.JCClassDecl> getAppendNoArgsStrategy(TreeModifier treeModifier, AccessLevel accessLevel) {
TreeMaker treeMaker = treeModifier.getTreeMaker();
Names names = treeModifier.getNames();
return jcClassDecl -> {
ListBuffer<JCTree> newMembers = new ListBuffer<>();
for (JCTree member : jcClassDecl.defs) {
/*
* 컴파일러는 기본적으로 파라미터 있는 생성자가 없다면, 파라미터가 없는 public 생성자를 넣어주게 된다.
* 이럴 경우 생성자가 중복 정의되기 때문에 원하는 접근 제어 레벨의 생성자를 삽입할때 에러가 발생한다.
* 따라서 이미 매개변수 없는 생성자가 있다면 제외해야 한다.
* */
if (member instanceof JCTree.JCMethodDecl) {
// 메서드가 생성자인지 확인한다.
boolean isConstructor = ((JCTree.JCMethodDecl) member).name.toString().equals("<init>");
// 생성자이면서 파라미터가 있다면 다시 넣어준다.
if (isConstructor && !((JCTree.JCMethodDecl) member).getParameters().isEmpty()) {
newMembers.add(member);
} else if (!isConstructor) {
// 생성자가 아니라면 메서드이므로 추가한다.
newMembers.append(member);
}
} else {
// 메서드가 아닌 멤버는 전부 넣어준다.
newMembers.append(member);
}
}
jcClassDecl.defs = newMembers.toList();
JCTree.JCMethodDecl noArgs = createNoArgs(treeMaker, names, accessLevel);
jcClassDecl.defs = jcClassDecl.defs.append(noArgs);
};
}
기본 생성자를 만들어줄때의 문제는 몇가지가 있습니다.
컴파일러
는 생성자가 없을 경우, 기본 생성자
를 넣어줍니다. 또는 사용자가 직접 정의해둔 생성자가 있을 수 있습니다.
이럴 경우 생성자 중복 정의라는 컴파일 에러가 발생하게 됩니다.
따라서 멤버를 모두 순회하면서 멤버가 메서드라면, 생성자
인지 먼저 확인
하게 됩니다.
생성자라면 매개변수가 없는 생성자
일 경우, 넣어주지 않는 방식으로 컴파일러
가 기본적으로 만들어준 생성자를 AST에서 제외
시킵니다.
매개변수가 있는 생성자
또는, 일반 메서드
, 필드
등은 모두 List에 넣어주고 AST에 추가
시켜줍니다.
private JCTree.JCMethodDecl createNoArgs(TreeMaker treeMaker, Names names, AccessLevel accessLevel) {
long flag = 0L;
switch (accessLevel) {
case PUBLIC:
flag = 1L;
break;
case PACKAGE:
flag = 0L;
break;
case PRIVATE:
flag = 2L;
break;
case PROTECTED:
flag = 4L;
break;
}
return treeMaker.MethodDef(
treeMaker.Modifiers(flag),
names.init,
treeMaker.TypeIdent(TypeTag.VOID),
List.nil(),
List.nil(),
List.nil(),
treeMaker.Block(0, List.nil()),
null
);
}
매개변수 없는 생성자
메서드를 만드는 부분입니다.
accessLevel
의 값에 따라서 접근 제어 flag
값을 설정해주게 됩니다.
flag
값에 따라서 접근 제어 레벨
이 변경
되게 되는 것입니다.
메서드를 만드는 부분에서도, 본문과 return Type
이 없고 이름도 클래스의 이름
과 같기 때문에 간단한 코드로 메서드를 정의
할 수 있습니다.
열심히 만들었다면 이제 사용해봐야 하는 시간이겠죠?
한 번 사용해보겠습니다.
이제 application
프로젝트로 가서, build.gradle
을 작성
해줍니다.
dependencies
에서 annotation 프로젝트
를 의존
하고 있는 것을 볼 수 있습니다.
plugins {
id 'java'
}
sourceCompatibility = 11
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testImplementation 'junit:junit:4.13.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
compileOnly project(':annotation')
implementation project(':annotation')
annotationProcessor project(':annotation')
testImplementation project(':annotation')
testAnnotationProcessor project(':annotation')
}
test {
useJUnitPlatform()
}
@Getter
와 @Setter
애너테이션을 붙인 Car 클래스
를 정의해보겠습니다.
@Getter
@Setter
public class Car {
private String name;
private int price;
}
컴파일된 바이트 코드를 디컴파일해서 보면, Getter & Setter
가 생성되어 있는 것을 볼 수 있습니다.
public static void main(String[] args) {
Car car = new Car();
car.setName("benz");
car.setPrice(2000);
System.out.println(car.getName()); // benz
System.out.println(car.getPrice()); // 2000
}
코드는 정상 동작하는 것을 볼 수 있습니다.
인텔리제이를 사용중이라면 편집기에서 빨간색으로 표시
되는 문제를 만날 수 있습니다.
하지만 코드는 정상 동작
하기 때문에 신경쓰지 않아도 됩니다.
Lombok
은 인텔리제이에서 플러그인을 제공
해 이런 문제가 없지만, 직접 만든 Annotation Processor
의 경우 편집기에서 인식하지 못한다고 합니다.
마지막으로 NoArgsConstructor
를 테스트 해보겠습니다.
@NoArgsConstructor(accessLevel = AccessLevel.PROTECTED)
public class Car {
private String name;
private int price;
}
Protected
레벨의 생성자
가 만들어지는지 확인해 보겠습니다.
@NoArgsConstructor(accessLevel = AccessLevel.PRIVATE)
public class Car {
private String name;
private int price;
}
Private
레벨의 생성자
도 한 번 테스트 해보겠습니다.
잘 동작하는 것을 확인할 수 있습니다.
이 모든 코드는 레포에 올려두었으니 언제든 확인하실 수 있습니다.
Lombok
을 사용하면 얼마나 개발 생산성이 올라가는지 몸소 체험하고 있기 때문에 늘 고마운 존재
였습니다.
Lombok
의 내부 동작 원리
를 보면서, 가져다 쓰는 것은 쉬웠지만 내부에는 심오한 기술들이 많이 숨어있었구나를 느끼게 되었습니다.
앞으로도 사용하는 기술에 대해 사용을 넘어서서 꼭 내부 구현
을 확인하고 직접 이해보는 시간을 가져야겠다는 확신을 가지게 된 좋은 실험
이였던 것 같습니다.
오늘도 읽어주셔서 감사합니다!
이런 변태같은 개발자...