기본 콘텐츠로 건너뛰기

Lombok은 어떻게 동작되나? 간단정리

lombok은 Annotation Processing과 Instrumentation을 사용한다. Instrumentation은 이클립스는 별도 컴파일러가 있기 때문에 이클립스 종속적인 bytecode 처리에 사용되고, AnnotationProcessor에서는 Annotation별로 코드를 생성하는데 사용된다.

Annotation별 코드 생성이란, 예를 들어 @Getter를 선언하면 실제 getter가 생성된다.
@Getter String name;

public String getName() {
    return this.name;
}

간단히 lombok구현은 AnnotationProcessor를 이용한 컴파일 시점 코드 생성으로 요약 할 수 있다.

AnnotationProcessor는 Java Compiler와 관련이 있다. 잠시 정리해 보자


javac는 크게 두가지 부분으로 나눈다. 하나는 ".java"을 ".class"로 바꾸는 컴파일러이고 다른 하나는 컴파일러를 지원하는 환경이다.

컴파일러 환경은 세가지 API 패키지를 제공하고
  • Language Model API ( javax.lang.model package ) 
  • Annotation Processing API ( javax.annotation.processing package ) 
  • Compiler Tree API( com.sun.source package )
컴파일러는 Java Compiler API 패키지를 제공한다.
  • Java Compiler API ( javax.tools package ) 

javac는 세가지 방식으로 호출 할 수 있다.
  • com.sun.tools.javac.main.Main: 커멘드라인에서 직접 호출 
  • java Compiler API: javax.tools 패키지 이용 
  • Compiler Tree API: javax.tools.CompilationTask를 com.sun.source.util.JavacTask로 바꾸는 방법 
컴파일 과정에서 생성된 Syntax Tree는 com.sun.source.tree.*에서 public access를 제공하며,

  • com.sun.source.util.Trees가 대표적이다. 일반적인 Tree API와 유사한 API를 제공한다. 

Data Model은 com.sun.tools.javac.code.* 에서 Type별 Semantic 정보를 제공한다. - Symbol, Type, Annotations, Atrribute등으로 javax.lang.model.element.Element를 implement 하고 있다.
  • Element 는 기본적으로 포함된 자식 element 들을 얻을 수 있지만, Symbol 과 같은 subtype에서는 코드의 위치 static여부 등의 상세한 내용들을 포함하고 있다. 
  • Enter, MemberEnter, Annotation Visitor에서 생성된다. Enter Visitor는 클래스 스캔을 MemberEnter Visitor는 클래스의 멤버 스캔을 그리고 Annotation Visitor는 Annotation을 스캔한다. 


Annotation ProcessingEnter, MemberEnter 스캔 이후에 시작한다.
  • Annotation Processing 중에는 컴파일에 필요한 부가적인 타입을 생성 
  • 분석 메소드 시그니쳐도 실행 할 수도 있다. 
  • 만약 Annotation Processing중 사용자가 파일을 새로 생성하면 컴파일은 재시작 된다. - 이 부분은 정확하지 않다. :)

컴파일의 단계의 마지막으로 ".class" 생성에는 com.sun.tools.javac.jvm.* 이 이용된다.
흐름을 정리하면, 컴파일 하면 컴파일러는 AST를 생성시킨 후 "javac -processor"에 등록된 AnnotationProcessor를 호출한다. AnnotationProcessor에서는 컴파일러에서 제공된 Syntax Tree를 이용 하거나 직접 Visitor를 구현해서 트리를 탐색하며 필요한 데이터를 얻고 com.sun.source.tree.* 패키지를 이용해서 필요한 코드를 추가 할 수 있다.


lombok 기본 원리인 AnnotationProcessor를 이용한 코드생성 예제

HeloWorld라는 Annotation을 하나 선언한다.
public @interface HelloWorld {
}

그리고 AnnotationProcessor를 생성한다.

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;

@SupportedAnnotationTypes({ "HelloWorld" })
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class HelloProcessor extends AbstractProcessor {

 @Override
 public boolean process(
   Set<? extends TypeElement> annotations, 
   RoundEnvironment roundEnv) {
  
  if (roundEnv.processingOver()) {
   return false;
  }
  
  JavacProcessingEnvironment environment 
   = (JavacProcessingEnvironment) processingEnv;
  
  Context context 
   = environment.getContext();
  TreeMaker maker 
   = TreeMaker.instance(context);
  JavacElements elemUtils 
   = (JavacElements) processingEnv.getElementUtils();
  
  Class<HelloWorld> clazz = HelloWorld.class;
  Set<? extends Element> elements 
   = roundEnv.getElementsAnnotatedWith(clazz);
  
  for (Element element : elements) {
   JCTree tree = elemUtils.getTree(element);
   JCTree.JCMethodDecl mDecl 
    = (JCTree.JCMethodDecl) tree;
   
   injectHelloWorld(maker, elemUtils, mDecl);
  }
  
  return false;
 }

 private void injectHelloWorld(
   TreeMaker maker, 
   JavacElements elemUtils, 
   JCTree.JCMethodDecl mDecl) {
  
  maker.pos = mDecl.pos;
  
  List<JCExpression> nil = List.<JCTree.JCExpression> nil();
  Name system = elemUtils.getName("System");
  Name out = elemUtils.getName("out");
  Name _println = elemUtils.getName("println");
  JCLiteral helloworld = maker.Literal("Hello world");
  
  mDecl.body = maker.Block(0,List.of(
   maker.Exec(maker.Apply(nil,
    maker.Select(maker.Select(
     maker.Ident(
     system),
     out),
     _println),
    List.<JCTree.JCExpression> of(helloworld))),
   mDecl.body)
  );
 }
}

마지막으로 @HelloWorld 를 사용하는 코드를 하나 만든다.

public class Dummy {

 private String name;

 @HelloWorld
 public String getName() {
  return this.name;
 }

 public static void main(String... args) {
  new Dummy().getName();
 }
}

컴파일을 하면 Hello World가 출력 된다.

javac \
-cp target/apt-playground-1.0-SNAPSHOT.jar \
-processor HelloProcessor \
Dummy.java


전체 코드 

https://gist.github.com/freestrings/e733342c3b4e78a050dc


lombok AnnotationProcessor 참고 링크 



댓글