커스텀 컴파일러 플러그인
커스텀 컴파일러 플러그인 (Custom compiler plugins)
컴파일러 자체를 수정하지 않고도 컴파일 과정에 끼어들어 코드를 분석하거나 바꿀 수 있는 방법이 있는데, 바로 컴파일러 플러그인이에요. 예를 들어 어떤 프레임워크나 API와 호환되도록 코드에 애너테이션을 붙이거나 새 코드를 생성할 수 있죠. 다만 Kotlin 컴파일러 플러그인 API는 아직 불안정해서 릴리스마다 호환되지 않는 변경이 생길 수 있다는 점을 미리 알아두실 필요가 있어요.
본문
Kotlin 컴파일러 플러그인 API는 불안정하며 릴리스마다 호환되지 않는 변경을 도입해요.
컴파일러 플러그인은 컴파일 과정에 끼어들어, 컴파일러 자체를 수정하지 않고도 코드가 컴파일되는 동안 코드를 분석하거나 바꿔요. 예를 들어 코드에 애너테이션을 붙이거나 새 코드를 생성해 다른 프레임워크나 API와 호환되게 만들 수 있어요.
커스텀 컴파일러 플러그인을 만들기 전에, 사용 가능한 컴파일러 플러그인 목록을 확인해서 이미 쓰임새에 맞는 게 있는지 살펴보세요. Kotlin Symbol Processing (KSP) API나 Android lint 같은 외부 linter로 목표를 달성할 수 있는지도 확인해 보세요.
그래도 필요한 것을 찾지 못했다면 커스텀 컴파일러 플러그인을 만들 수 있어요. 단, Kotlin 컴파일러 플러그인 API는 불안정해요. 새 컴파일러 릴리스마다 호환되지 않는 변경이 도입되므로, 유지 관리에 지속적으로 상당한 노력을 투자해야 한다는 점을 염두에 두세요.
Kotlin 컴파일러와 컴파일러 플러그인
- Kotlin 컴파일러는:
- 소스 코드를 파싱해서 구조화된 구문 트리(syntax tree)로 만든다.
- 코드를 분석·해석하여 의미를 파악하고, 이름을 해석하고, 타입을 검사하며, 가시성 규칙을 적용한다.
- 소스 코드와 기계어 사이의 다리 역할을 하는 데이터 구조인 중간 표현(Intermediate Representation, IR)을 생성한다.
- IR을 점진적으로 더 단순한 형태로 낮춘다(lower).
- 낮춰진 IR을 JVM 바이트코드, JavaScript, 네이티브 기계어 같은 타깃별 출력으로 변환한다.
플러그인은 프론트엔드 API를 통해 초기 컴파일러 단계에 영향을 줄 수 있어요. 컴파일러가 코드를 어떻게 해석하는지 바꾸는 거죠. 예를 들어 플러그인이 애너테이션을 추가하거나, 본문이 없는 새 메서드를 도입하거나, 가시성 수정자를 바꿀 수 있어요. 이런 변경은 IDE에서도 볼 수 있어요.
플러그인은 백엔드 API를 통해 이후 단계에도 영향을 줄 수 있어요. 선언의 동작을 수정하는 거죠. 이런 변경은 컴파일이 끝난 뒤 만들어진 바이너리에 나타나요.
실제로 컴파일러 플러그인은 분석·해석 단계부터 코드 생성까지, 프론트엔드와 백엔드를 아우르는 단계에 영향을 미쳐요. 예를 들어 프론트엔드 부분이 선언을 생성하고, 백엔드 부분이 그 선언들에 본문을 붙이는 식이에요.
Kotlin serialization 플러그인이 좋은 예시예요. 플러그인의 프론트엔드 부분은 컴패니언 객체(companion object)와 serializer 함수를 추가하고, 이름 충돌을 막는 검사도 넣어요. 백엔드 부분은 KSerializer 객체를 통해 원하는 직렬화 동작을 구현해요.
Kotlin 컴파일러 플러그인 템플릿
커스텀 컴파일러 플러그인 작성을 시작하려면 Kotlin 컴파일러 플러그인 템플릿을 사용하면 돼요. 그다음 프론트엔드와 백엔드 플러그인 API에서 확장 포인트(extension point)를 등록해요.
현재 커스텀 컴파일러 플러그인은 Gradle로만 개발할 수 있어요.
프론트엔드 플러그인 API
프론트엔드 플러그인 API(FIR, 프론트엔드 중간 표현)에는 해석(resolution)을 커스터마이즈할 수 있는 다음과 같은 전용 확장 포인트가 있어요.
| Extension name | Description |
|---|---|
FirAdditionalCheckersExtension |
커스텀 컴파일러 검사기를 추가한다. |
FirDeclarationGenerationExtension |
새 선언을 생성한다. |
FirExtensionSessionComponent |
플러그인의 다른 부분이 쓰도록 FirSession에 커스텀 컴포넌트를 등록한다. |
FirFunctionTypeKindExtension |
함수형 타입의 새 family를 정의한다. |
FirMetadataSerializerPlugin |
선언 메타데이터에 정보를 읽고 쓴다. |
FirStatusTransformerExtension |
가시성이나 modality 같은 선언 상태 속성을 수정한다. |
FirSupertypeGenerationExtension |
기존 클래스에 새 슈퍼타입을 추가한다. |
FirTypeAttributeExtension |
타입 애너테이션에 기반해 특정 타입에 특수 속성을 추가한다. |
IDE 통합
해석(resolution) 변경은 코드 하이라이팅이나 제안 같은 IDE 동작에 영향을 주므로, 플러그인이 IDE와 호환되는 것이 중요해요. IntelliJ IDEA와 Android Studio의 각 버전에는 Kotlin 컴파일러의 개발 버전이 포함돼 있어요. 이 버전은 IDE에 특화된 것으로, 출시된 Kotlin 컴파일러와 바이너리 호환되지 않아요. 그 결과 IDE를 업데이트하면 플러그인이 계속 동작하도록 컴파일러 플러그인도 함께 업데이트해야 해요. 이런 이유로 커뮤니티 플러그인은 기본적으로 로드되지 않아요.
커스텀 컴파일러 플러그인이 다양한 IDE 버전에서 동작하는지 확인하려면 각 IDE 버전에서 테스트하고 발견한 문제를 고쳐야 해요.
Kotlin 컴파일러 플러그인용 devkit이 있다면 여러 IDE 버전을 지원하는 게 더 쉬워질 수도 있어요. 이 기능에 관심이 있다면 이슈 트래커에서 피드백을 남겨 주세요.
백엔드 플러그인 API
백엔드 플러그인 개발은 IDE나 디버거 성능을 떨어뜨리지 않으면서 올바르게 하기가 어려우므로, 변경에 신중하고 보수적으로 접근하세요.
백엔드 플러그인 API(IR)에는 확장 포인트가 하나뿐이에요: IrGenerationExtension. 이 확장 포인트를 사용하고 generate() 함수를 오버라이드해서, 프론트엔드가 이미 생성한 선언들에 본문을 추가하거나 기존 선언 본문을 바꿔요.
이 확장 포인트를 통한 변경은 컴파일러가 검사하지 않아요. 이 단계에서 컴파일러의 기대를 깨뜨리지 않도록 직접 확인해야 해요. 예를 들어 실수로 잘못된 타입, 잘못된 함수 참조, 올바른 스코프 밖의 참조를 도입할 수 있어요.
백엔드 플러그인 코드 탐색하기
Kotlin serialization 플러그인 코드를 보면 백엔드 플러그인 컴파일러 코드가 실제로 어떤 모습인지 탐색할 수 있어요. 예를 들어 SerializableCompanionIrGenerator.kt는 핵심 serializer 멤버의 누락된 본문을 채워 넣어요. 한 예로 generateChildSerializersGetter() 함수는 KSerializer 표현식 목록을 모아 배열로 반환해요.
백엔드 플러그인 코드에 문제가 없는지 확인하기
백엔드 플러그인 코드의 문제를 확인하는 방법은 세 가지가 있어요.
- IR 빌드 검증 — IR 트리를 빌드하고
Xverify-ir컴파일러 옵션을 켜요. 이 옵션은 컴파일 속도에 성능 영향을 주므로 테스트할 때만 사용하세요. - IR 출력 덤프·비교 —
-Xphases-to-dump-before=ExternalPackageParentPatcherLowering컴파일러 옵션으로 IR lowering 컴파일 단계 이후의 덤프 파일을 만들어요. JVM 백엔드에서는-Xdump-directory=<your-file-directory>컴파일러 옵션으로 덤프 디렉터리를 구성해요. 기대되는 코드를 직접 작성하고 또 다른 덤프 파일을 만들어, 둘을 비교해 차이가 있는지 확인해요. - 컴파일러 코드 디버그 —
convertToIr.kt파일의convertToIrAndActualize()함수에 중단점(breakpoint)을 추가하고 디버그 모드로 컴파일러를 실행하면 컴파일 중 더 자세한 정보를 얻을 수 있어요.
플러그인 테스트하기
플러그인을 구현한 뒤에는 철저히 테스트해요. Kotlin 컴파일러 플러그인 템플릿은 이미 Kotlin 컴파일러 테스트 프레임워크를 사용하도록 설정되어 있어요. 다음 디렉터리에 테스트를 추가할 수 있어요.
compiler-plugin/testDatacompiler-plugin/testData/box— 코드 생성 테스트용compiler-plugin/testData/diagnostics— 진단 테스트용
테스트를 실행하면 프레임워크가:
- 테스트 소스 파일을 파싱한다. 예:
anotherBoxTest.kt - 각 파일에 대한 FIR과 IR을 빌드한다.
- 이것들을 텍스트 덤프 파일로 기록한다. 예:
anotherBoxTest.fir.txt와anotherBoxTest.fir.ir.txt - 이전에 만든 파일이 있으면 그 파일과 비교한다.
이 파일들로 생성된 diff에 의도하지 않은 변경이 있는지 확인할 수 있어요. 문제가 없다면 새 덤프 파일이 최신 골든 파일(golden file)이 돼요. 즉 승인되고 신뢰할 수 있는 기준이 되어, 이후 변경 사항을 그 기준과 비교할 수 있어요.
도움 받기
커스텀 컴파일러 플러그인을 개발하다 문제가 생기면 Kotlin Slack의 #compiler 채널에 문의해 보세요. 해결책을 약속할 수는 없지만, 가능하다면 돕도록 노력할게요.
더 알아보기
- 사용 가능한 컴파일러 플러그인 목록 — 이미 있는 플러그인 확인
- Kotlin 컴파일러 플러그인 템플릿 — 플러그인 개발 시작점
- KSP 개요 — 심볼 처리 API