개발/Java

Lombok의 동작원리

하프킴 2021. 5. 13. 13:56
728x90

Lombok은 컴파일 시점에 바이트코드를 변환하여 원하는 부분을 주입해주는 방식으로 동작한다. 

 

아래에서 더 자세히 알아보자.

 

Lombok의 프로세스 과정

Lombok이 처리되는 과정은 다음과 같다

 

1. javac는 소스파일을 파싱하여 AST트리를 만든다.

 

2. Lombok은 AnnotaionProcessor에 따라 AST 트리를 동적으로 수정하고 새 노드(소스코드)를 추가하고 마지막으로 바이트 코드를 분석 및 생성한다.

 

(컴파일 과정에서 생성된 Syntax Tree는 com.sun.source.tree.*에서 public accesss를 제공한다.)

 

4. 최종적으로 javac는 Lombok Annotation Processor에 의해 수정된 AST를 기반으로 Byte Code를 생성한다.

 

 

코드레벨에서 더 자세히 알아보자

 

우선 아래의 코드는 lombok.core.AnnotationProcess.java의 process 함수이다.

아래에서 재귀적으로 루트에서부터 순회를 하는 것을 볼 수 있다.

특히 RoundEnvironment의 rootElements()를 통해 자바 컴파일러가 생성한 AST를 참조한다.

(아래 깃헙 레포에서 확인할 수 있다) github.com/projectlombok/lombok/blob/5120abe4741c78d19d7e65404f407cfe57074a47/src/core/lombok/core/AnnotationProcessor.java)

	@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
		if (!delayedWarnings.isEmpty()) {
        // 루트 어노테이션을 참조한다.
			Set<? extends Element> rootElements = roundEnv.getRootElements();
           
           // 루트 어노테이션에서부터 순회한다.
			if (!rootElements.isEmpty()) {
				Element firstRoot = rootElements.iterator().next();
				for (String warning : delayedWarnings) processingEnv.getMessager().printMessage(Kind.WARNING, warning, firstRoot);
				delayedWarnings.clear();
			}
		}
		
		for (ProcessorDescriptor proc : active) proc.process(annotations, roundEnv);
		
		boolean onlyLombok = true;
		boolean zeroElems = true;
		for (TypeElement elem : annotations) {
			zeroElems = false;
			Name n = elem.getQualifiedName();
			if (n.toString().startsWith("lombok.")) continue;
			onlyLombok = false;
		}
		
		// Normally we rely on the claiming processor to claim away all lombok annotations.
		// One of the many Java9 oversights is that this 'process' API has not been fixed to address the point that 'files I want to look at' and 'annotations I want to claim' must be one and the same,
		// and yet in java9 you can no longer have 2 providers for the same service, thus, if you go by module path, lombok no longer loads the ClaimingProcessor.
		// This doesn't do as good a job, but it'll have to do. The only way to go from here, I think, is either 2 modules, or use reflection hackery to add ClaimingProcessor during our init.
		
		return onlyLombok && !zeroElems;
	}

 

 

아래에서 RoundEnviroment 인터페이스 코드를 열어보면

getRootElements()가 정의되어 있고, 앞선 round에서 생성된 root 어노테이션을 참조를 리턴한다.

 

(RoundEnvironment인터페이스는 Annotation processing tool 프레임워크이고, 프로세서가 annotation processing의 라운드를 수정할 수 있도록 도와줌.)

/**
 * An annotation processing tool framework will {@linkplain
 * Processor#process provide an annotation processor with an object
 * implementing this interface} so that the processor can query for
 * information about a round of annotation processing.
 *
 * @author Joseph D. Darcy
 * @author Scott Seligman
 * @author Peter von der Ah&eacute;
 * @since 1.6
 */
public interface RoundEnvironment {}
    /**
     * Returns the {@linkplain Processor root elements} for annotation processing generated
     * by the prior round.
     *
     * @return the root elements for annotation processing generated
     * by the prior round, or an empty set if there were none
     */
    Set<? extends Element> getRootElements();

    /**
     * Returns the elements annotated with the given annotation type.
     * The annotation may appear directly or be inherited.  Only
     * package elements, module elements, and type elements <i>included</i> in this
     * round of annotation processing, or declarations of members,
     * constructors, parameters, type parameters, or record components
     * declared within those, are returned.  Included type elements are {@linkplain
     * #getRootElements root types} and any member types nested within
     * them.  Elements of a package are not considered included simply
     * because a {@code package-info} file for that package was
     * created.
     * Likewise, elements of a module are not considered included
     * simply because a {@code module-info} file for that module was
     * created.
     *
     * @param a  annotation type being requested
     * @return the elements annotated with the given annotation type,
     * or an empty set if there are none
     * @throws IllegalArgumentException if the argument does not
     * represent an annotation type
     */
    Set<? extends Element> getElementsAnnotatedWith(TypeElement a);

 

 

요약을 하면

 

Lombok은 컴파일 타임에 AnnotaionProcessor에 따라 AST 트리를 동적으로 수정하고 새 노드(소스코드)를 추가하고 마지막으로 바이트 코드를 분석 및 생성한다.

 

 

마지막으로 직접 Lombok을 통해 변환된 코드를 확인해보자.

IntelliJ에서 기본적으로 제공되는 디컴파일러를 이용해 직접 바이트코드를 확인할 수 있다.

 

아래와 같이 클래스를 만들고 빌드를 하면

아래의 디렉토리에 .class 파일이 생성된다.

생성된 .class 확인해보면 아래와 같이 Lombok에 의해 모든 변수를 초기화하는 생성자와, getter가 생성된 것을 확인할 수 있다.

 

추상화된 Lombok의 동작원리를 내부 코드레벨까지 살펴보았다. 이제 어느정도 확신을 가지고 Lombok을 사용할 수 있게 되었다.

 

 

 

<참고>

 

(*아래의 그림처럼 javac에 의해 Annotation은 여러 라운드에 걸쳐 processing 된다.)

여러 Round에 걸쳐 처리되는 Annotaion

 

 

 

*Abstract Syntax Tree 추상구문트리란?

추상이라는 말은 실제 구문에서나타나는 모든 세세한 정보를 나타내지 않는다는 것을 의미함.

주로 컴파일러에서 널리 사용되는 자료구조.

Abstract Syntatic 구조를 표현하기 위해서 사용된다.

Abstract Syntatic이란 프로그래밍 언어의 문법 및 각 문단의 역할을 표현하기 위한 규칙.

프로그래밍 언어의 사용이 틀린 부분이 없는지, 문맥적인 소스코드 검사의 단계에서 사용됨.

 

 

 

 

 

 

 

 

출처

free-strings.blogspot.com/2015/12/lombok.html

www.programmersought.com/article/42205547853/

pplenty.tistory.com/13#:~:text=%EB%A1%AC%EB%B3%B5(lombok)%EC%9D%98%20%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC,%EC%9D%84%20%EC%A3%BC%EC%9E%85%ED%95%B4%EC%A3%BC%EB%8A%94%20%EB%B0%A9%EC%8B%9D%EC%9D%B4%EB%8B%A4.