최근 프로젝트를 진행하며 롬복의 동작 원리에 대해 복습할 일이 있었는데 이에 대해 간략하게 정리해보겠습니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.
1. AST(Abstract Syntax Tree)
롬복을 알려면 우선 AST(Abstract Syntax Tree)에 대해 알아야 합니다. 이는 프로그래밍 언어로 작성된 소스 코드의 추상 구문 구조의 트리입니다. 각 노드는 소스 코드에서 발생하는 구조를 나타내는데, 이는 어느 정도의 구조를 나타내긴 하지만 실제 구문에서 나타나는 모든 정보를 나타내지는 않습니다. 즉 요약해 보면 AST는 프로그램 코드의 구조를 추상적으로 표현하는 프로퍼티로 컴파일러의 구문 분석 결과물이라고 할 수 있습니다.
예를 들어 아래 클래스가 AST로 어떻게 나타나게 되는지 한 번 살펴보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class StudyWithMeUser {
private final Long userId;
public StudyWithMeUser(Long userId) {
this.userId = userId;
}
public Long getUserId() {
return userId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof StudyWithMeUser that)) return false;
return getUserId().equals(that.getUserId());
}
@Override
public int hashCode() {
return Objects.hash(getUserId());
}
@Override
public String toString() {
return userId.toString();
}
}
AST를 보면 다음과 같은데요, 우리가 작성한 클래스 파일이 갈래로 나뉘어 분류된 것을 볼 수 있습니다.
그림으로 나타내면 대략 아래와 같습니다. 우리가 작성한 코드가 AST에서 분류된 것입니다.
그런데 이게 왜 나와? 라고 생각하실 수 있는데, 롬복을 사용하면 바이트코드를 조작해서 AST에 이를 이어 붙이기 때문 입니다. 아래와 같이 롬복을 사용하도록 코드를 수정 해보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import lombok.Getter;
import java.util.Objects;
// Getter 사용
@Getter
public class StudyWithMeUser {
private final Long userId;
public StudyWithMeUser(Long userId) {
this.userId = userId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof StudyWithMeUser that)) return false;
return getUserId().equals(that.getUserId());
}
@Override
public int hashCode() {
return Objects.hash(getUserId());
}
@Override
public String toString() {
return userId.toString();
}
}
그러면 Imports에 아까는 없던 롬복 관련 Import가 생긴걸 볼 수 있는데요, 즉 롬복의 메서드를 가져와서 AST에 붙이는 것입니다. 트리를 보면 @Getter에 대한 Imports가 생긴 것을 볼 수 있습니다.
그러면 우리는 Getter를 수동으로 구현하지 않아도 롬복이 제공해 주는 @getter 메서드를 이용할 수 있게 됩니다.
컴파일 된 바이트 코드를 보더라도 아래와 같이 Getter 메서드가 Import 된 것을 볼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// class version 61.0 (61)
// access flags 0x21
public class project/swithme/order/common/auth/StudyWithMeUser {
// compiled from: StudyWithMeUser.java
......
// access flags 0x1
public getUserId()Ljava/lang/Long;
L0
LINENUMBER 10 L0
ALOAD 0
GETFIELD project/swithme/order/common/auth/StudyWithMeUser.userId : Ljava/lang/Long;
ARETURN
L1
LOCALVARIABLE this Lproject/swithme/order/common/auth/StudyWithMeUser; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
2. 정리
원래는 바이트코드는 읽을 수만 있고 조작은 할 수 없습니다. 즉 AST는 Read는 가능하지만, Write는 불가능한데요, 이를 가능하게 해주는 것이 어노테이션 프로세서(Annotation Processor) 입니다. 어노테이션 프로세서는 특정한 어노테이션이 붙은 소스 코드를 컴파일할 때 참조해 새로운 소스를 만들 수 있는 기능을 제공합니다. 따라서 이를 이용해 롬복의 어노테이션 AST를 조작하는 것입니다. 글이 너무 길었는데 그 과정을 요약해보겠습니다.
- javac은 소스파일을 파싱해 AST를 만듭니다.
- 롬복은 어노테이션 프로세서를 통해 AST를 동적으로 수정 후 바이트 코드를 생성합니다.
- javac은 어노테이션 프로세서에 의해 수정된 AST를 기반으로 바이트 코드를 생성합니다.
- 이후 컴파일 된 클래스에서 이를 사용할 수 있습니다.