개발 포기 !

개발 포기 !

세상만사 쉬운게 하나도 없다


뭘 포기하는데?


JPA에서 DB 테이블과 매핑하기 위해 사용하는 엔티티는 접근제한자가 private이 아닌 기본 생성자가 반드시 필요하다.

그저께 엔티티에 무지성으로(그리고 기계적으로) @NoArgsConstructor를 살포하고 있다가, @Entity를 감지하면 접근제한자가 protected 인 기본 생성자를 자동으로 만들어주는 도구를 만들어 보면 어떨까? 그럼 @NoArgsConstructor 안 써도 되니까 이거 완전 개꿀인 부분 아닌가? 하는 생각이 문득 들었다.


그리고 그렇다면 이것을 어떻게 만들것인지 곰곰이 생각을 또 해보니 "이거 그냥 @Entity 감지해서 기본생성자 하나 달아주면 끝이네? 개 쉽네?" 라고 생각하고 바로 작업에 착수했다.

당시 내가 고려해야 할 사항은 다음과 같이 단순했다.


  • 컴파일 타임에 매개변수가 없는 기본 생성자를 추가해주면 된다
    • 프록시 기반의, 런타임에 바이트 코드를 조작하는 방식들(ASM, Byte Buddy 등)은 사용할 수 없다
    • 그럼 annotation processor를 사용하면 되겠다


image


이후 MVP를 만들어 로컬 저장소에 배포해서 돌려보니 이게 웬걸?

이상한 예외가 발생했다.

예외 메시지를 천천히 살펴보니 annotation processor는 새로운 .class파일은 만들수 있지만, 기존에 존재하는 .class파일을 수정 할 수는 없다는 것 같았다.

바로 현실부정에 들어가며 스펙들을 찾아보기 시작했는데…


During each run of an annotation processing tool, a file with a given pathname may be created only once. If that file already exists before the first attempt to create it, the old contents will be deleted. Any subsequent attempt to create the same file during a run will throw a FilerException, as will attempting to create both a class file and source file for the same type name or same package name. The initial inputs to the tool are considered to be created by the zeroth round; therefore, attempting to create a source or class file corresponding to one of those inputs will result in a FilerException.

annotation processor를 실행할 때마다 지정된 경로명을 가진 파일은 단 한 번만 생성될 수 있습니다. 파일을 처음 생성하기 전 해당 파일이 이미 존재하는 경우 이전 내용이 삭제됩니다. 이후 실행 중에 동일한 파일을 생성하려고 하면 FilerException이 발생합니다. 이는 동일한 타입 이름 또는 동일한 패키지 이름에 대한 클래스 파일과 소스 파일을 모두 생성하려고 시도하는 것이기 때문입니다. 툴에 대한 초기 입력은 0번째 라운드에서 생성된 것으로 간주됩니다. 따라서 이러한 입력 중 하나에 해당하는 소스 또는 클래스 파일을 만들려고 하면 FilerException이 발생합니다.


진짜 그렇다.


image


바로 의문이 들었는데, 그럼 롬복(Lombok)은 대체 이걸 어떻게 하는거지?

당장 롬복 깃허브에 들어가 프로젝트를 로컬 머신에 클론하고, 소스코드를 살펴보기 시작했다.

이윽고 어이가 없어져버렸는데, 롬복은 무려 자바 컴파일러를 해킹하고 있었다.


image


자바 컴파일러가 자바 코드를 구문분석해 이를 트리구조로 만들어 관리하는데, 이를 추상 구문 트리(AST, Abstract Syntax Tree) 라고 부르는 듯 했다. (JCTree가 AST의 루트 노드라는데, 이 녀석 이름 뜻은 Java Code Tree인가? 🤔)

관련 자료들을 좀 둘러보다 보니 eslint같은 정적 코드 분석 도구들이 돌아가는 방식도 이와 비슷한 원리로 추측되었다.


아무튼 뭐가 문제냐면, 이 AST를 조작하는 API가 정식적인 공개 API가 아니고, 비공개 API 라는 것이다.

한마디로 이것들은 자바팀에서 사용하지 말라고 숨겨놓은 것들인데, 롬복은 이를 리플렉션을 통해 강제로 끄집어내 사용하고 있었다. (tools.jar, com.sun.tools 패키지의…)

그리고 getter, setter등의 노드를 만들어 AST에 append하는 방식으로 돌아가는 듯 했다.

그런데 공개 API는 외부에 직접적으로 노출되는 인터페이스이기 때문에 하위호환성을 위해 변경되지 않는게 원칙이지만, 비공개 API들은 세부 구현이기 때문에 언제든지 변경될 수 있다는게 문제다.

즉, 롬복은(그리고 내가 하려던 짓은) 이 언제든지 변경될 수 있는 비공개 API들을 어거지로 끌어다 사용하고 있기 때문에, 자바의 세부 구현에 직접적으로 영향을 받고 있는 셈이었다.

초창기의 롬복이 굉장히 불안정하고, 호환성이 좋지 않았던 이유가 이와 관련되지 않았을까?


그래서 ?


일단 컴파일러와 AST에 대한 내 이해도도 처참한 수준이라, 내게는 구현 난이도부터 매우 높거니와(넘사벽) 어찌어찌 맨땅에 헤딩해가며 구현을 했다 하더라도 자바의 세부 구현이 변경 될 때마다 같이 유지보수를 해야 한다는 생각이 드니, 도저히 이걸 더 진행하지 못하겠다는 생각이 들었다.

나중에 자바가 계속 개선되어 AST를 조작할 수 있는 정식적인 API가 나오게 된다면 그 때 다시 천천히 공부를 해 봐야겠다.


그리고 사실 롬복이 별거 아닌 줄 알았다.

나는 "그까이꺼 그냥 annotation processor 몇개 구현하면 되는거 아니야?" 라는 무식한 생각을 갖고 있었다. (무지에서 나오는 용기…)

근데 코드를 뜯어보고 원리를 파헤쳐보니 정말 자바로 할 수 있는 최고 수준의 프로젝트가 아닌가 ?


암튼 세상만사 쉬운거 하나 없다.

그래도 이번 삽질에 annotation processor로 어디까지 할 수 있고, 어디까지 할 수 없는지 등 생각보다 많은 걸 배운 것 같기도 하다.


image


📕 Reference




© 2022. All rights reserved.