Lombok의 작동원리 (feat. 나만의 Lombok 만들기)

유알·2024년 6월 3일
1

연구실 발표 때 했던 내용을 블로그에 정리한 글이다.

자바를 사용하지 않는 사람들에게 자바에 대해 물으면, 많이 말하는 말이 "길다"라는 말이다.
이를 대응하기 위해 Java에는 Lombok이라는 라이브러리를 통해 이 문제를 해결하고 있다.

실제로 최근 Java 21에서 는 귀여운 업데이트도 있었는데, main을 아주 간단하게 작성할 수 있게 되었다. (사실 많이쓸까는 잘 모르겟지만, 우리도 짧을 수 있어라고 호소하는 느낌이라 귀엽다)


Lombok을 사용하면, 여러 boilerPlate들을 자동으로 만들어준다. 우리가 할 것은 단순히 어노테이션만 붙여주면 된다.

근데 어떻게 이게 가능한지 궁금했다.

그래서 Lombok의 소스코드를 까 보고, 나만의 간단한 버전의 롬복을 만들어 보았다.

우선 이것을 이해하기 위해서는 두가지 개념이 필요하다.

Annotation Processor를 만드려면 AbstractProcessor 를 구현해서 컴파일러 옵션에 제공하면 된다.

Java코드가 컴파일되는 과정을 보자, .java로 끝나는 소스코드를 제출하면 우선 Parser가 AST(추상 구문 트리)를 만들게 된다. 이것이 추후에 .class로된 바이트 코드로 컴파일이 될 예정이다.

하지만 그 가운데 무언가가 있는데, 바로 AnnotationProcessor이다. AnnotationProcessor는 구현하기에 따라 다양한 동작을 할 수 있는데, 코드를 검사하거나, 컴파일 과정에서 특정 메시지를 보낼 수도 있다.

아까 그 그림은 조금 설명이 부족해서 대가 추가로 만들어 보았다.

중요한 것은 AST다.

더 신기한 점은 AnnotationProcessor에서 소스코드를 새롭게 생성할 수 있다는 점인데, 소스코드가 새롭게 생성되면, 다시 Parser로 가서 이 또한 컴파일이 된다.

그러면 이 기술을 통해 Lombok이 동작하는거네? 아니다. 새롭게 소스코드를 생성한다는 것은 어디까지나 새로운 java 파일을 만드는 것에 해당되며, 이미 작성된 것을 수정하는 것은 불가능하다. Lombok이 하려는 것은 어노테이션을 붙인 클래스에 메서드나 생성자 같은 것을 수정하려는 것이다.

해킹이라고 한 점은 좀 어그로고, 정확히 말하면, JDK에서 허용하지 않는 API를 우회해서 호출한다.(이게 해킹인가?)

내가 만든 롬복을 보면, GETTER가 없는데도 불구하고(심지어 IDE에서는 에러까지 뜬다) 소스코드가 잘 돌아간다.

당연하다. 소스코드에는 없는 메서드가 바이트 코드에는 생성이 되기 때문이다.

자 생각해보자 우리의 어노테이션 프로세서가 AST를 조작해 버린다면 어떻게 될까? 그렇다 최종적으로 만들어지는 바이트 코드까지 조작할 수 있다. 바로 이 부분이 Lombok을 만들어낸 원리이다.

그리고 트리를 편리하게 수정하기 위한 클래스를 만들었는데, 이는 실제로 Lombok 소스코드를 까보면 유사한 코드가 있어서 그것을 참고했다.

그래서 나의 어노테이션 프로세서가 불법적인(?) 방법으로 GETTER 노드를 만들어서 AST노드에 밀어 넣는 것이다.

그러면 맨 마지막 단계에서 이 조작된 AST 노드를 기반으로 바이트 코드를 생성하게 된다.

롬복의 코드를 뒤져보다가 신기한 이슈를 발견했었는데, JDK 16 부터 Project Jigsaw(스펠링 맞나?,.,.) 에 의해 package의 접근 제어 모듈간 접근 제어가 가능해졌다. 쉽게 말하면 private public 하는 것처럼 패키지간 모듈간 접근을 제어할 수 있는 기술이다.

이게 도입되면서 롬복이 우회해서 쓰고 있던 API가 접근이 막히면서 Lombok에 큰 위기가 찾아온 적이 잇다.(근데 이걸 쓴다는 것도 좀 꺼림직하다. 내가 작성한 코드를 막 바꿀수 있다는 건데)

재밋는 댓글도 있었는데, 실제로 롬복 메인테이너가 JDK 개발자들한테 AST 노드를 고칠수 있는 합법적인 API 를 요청했지만 거절당했었다고 한다. (그러면 좀 롬복같은거를 java에서 지원을 해주던지)

어쨋든 해결한 커밋도 찾아봤는데, 코드가 상당히 어려워서 이해하기 힘들었지만, Java Reflection을 활용해서 강제로 모듈 캡슐화를 해제하는 것으로 보였다.

또 다른 코드 동적 생성 전략에는 CGLib이나 ByteBuddy 같은 것이 있었는데,

Byte Buddy의 근본이 되는 ASM 모듈(바이트 코드 직접 생성 라이브러리)에 공식문서에 가보니, Groovy나 Kotlin 컴파일러에서 이를 사용한다고 한다. 위 그림은 나의 상상속으로 그린것으로 아마 저런식으로 코틀린 컴파일러가 만들어진게 아닐가 생각을 해보았다.

profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글