[Kotlin] 소스코드가 컴파일되는 여정

Kame·2026년 1월 6일

Kotlin

목록 보기
10/11

안드로이드나 서버 개발을 하다 보면 @Composable, @Entity 같은 '어노테이션(Annotation)'을 자주 만납니다. 단순히 함수나 클래스 위에 이름표 하나 붙였을 뿐인데, 코드는 자동으로 데이터베이스를 구축하고 복잡한 UI를 최적화하여 그려냅니다.

이 때 Kotlin 컴파일러 내부에서 무슨 일이 벌어지고 있길래 이것이 가능한 것인지 의문이 들 것입니다.

오늘은 우리가 작성한 코드가 앱으로 변환되는 과정인 컴파일의 구조를 이해하고, ‘코드를 텍스트가 아닌 구조(IR)로 다룬다’ 는 것이 무엇인지, 어노테이션 프로세서와 컴파일러 플러그인의 차이를 알아보겠습니다.

선행 지식

코틀린 컴파일러를 설명하는 이 영상을 먼저 보고 오면 이해가 쉽습니다.


컴파일러 - 번역가이자 편집자

컴파일러는 단순히 소스 코드를 기계어(바이트코드)로 바꿔주는 번역기가 아닙니다. 비유하자면 현대의 컴파일러는 '출판사'와 같은 거대한 시스템이며, 크게 두 단계로 나뉩니다.

프론트엔드 (Frontend): 문법 검사관

우리가 쓴 소스 코드(.kt 파일)를 가장 먼저 만나는 곳입니다.

  • 하는 일: 맞춤법(문법) 검사, 단어의 적절성(타입) 확인
  • 결과물: 코드를 분석해서 IR(중간 표현)이라는 설계도를 만들어냅니다.
  • IDE의 역할: 인텔리제이에서 빨간 줄이 뜨는 것은, 컴파일러 프론트엔드가 실시간으로 이 분석을 수행하고 있기 때문입니다.

프론트엔드의 진단(diagnosis)

컴파일러 플러그인을 활용하면, 컴파일러의 분석 로직(Frontend)에 자신만의 규칙을 추가할 수 있습니다.

예를 들어, Compose 플러그인은 "Composable 함수 안에서는 일반 함수를 마음대로 호출하면 안 된다"는 규칙을 프론트엔드에 심어놓습니다. 그 이유로, 우리가 코드를 작성하자마자 인텔리제이(프론트엔드 탑재)가 이를 감지하고, Compose 규칙 위반임을 알리며 IDE에 오류를 표시할 수 있게 됩니다.

백엔드 (Backend): 인쇄소 기술자

프론트엔드가 넘겨준 IR(설계도)을 바탕으로 실제 결과물을 만듭니다.

  • 하는 일: 설계도를 보고 기계(JVM)가 이해할 수 있는 언어로 번역.
  • 결과물: .class 파일 (바이트코드) 또는 바이너리.

IR(Intermediate Representation)

컴파일러 플러그인이 코드를 고친다는데, 그럼 내 소스 파일(.kt)을 열어서 직접 수정하는 건가?

이 질문의 답을 찾기 위해서는 IR(Intermediate Representation)의 이해가 필요합니다.

직접 수정의 위험성

만약 컴파일러가 글자 그대로의 텍스트를 수정한다고 가정해 봅시다.

  • 원본: val a = 1 + 2
  • 조작: "1을 100으로 바꿔라"

만약 코드 어딘가에 val version = "v1"이라는 문자열이 있었다면? 컴퓨터는 문맥을 모르기 때문에 문자열 안의 1까지 100으로 바꿔버려 v100이 되는 대참사가 일어날 수 있습니다. 괄호 하나만 잘못 건드려도 프로그램 전체가 에러로 멈춥니다.

IR의 안정성

그래서 프론트엔드는 코드를 텍스트로 두지 않고, 의미 단위의 블록(객체)으로 변환하여 트리(Tree) 구조를 만듭니다. 이것이 바로 IR입니다.

  • 원본: val a = 1 + 2
  • IR 변환 결과 (트리 구조):
    • [변수 선언문] 노드
      • 이름: "a"
      • 값: [더하기 연산] 노드
        • 왼쪽: [숫자 1] 노드
        • 오른쪽: [숫자 2] 노드

이제 컴파일러 플러그인은 "텍스트에서 '1'을 찾아라"가 아니라, "트리에서 '[숫자 1] 노드'를 찾아서 '[숫자 100] 노드'로 교체해라"라고 명령합니다. 이렇게 하면 텍스트 파일은 그대로 둔 채, 컴파일러가 이해하는 로직만 완벽하고 안전하게 수정할 수 있습니다.


IR 활용 과정

이렇게 생성된 IR은 아래 단계에 따라 활용됩니다.

1. 분석 (Frontend)

프론트엔드가 .kt 텍스트 파일을 읽어 문법을 검사하고, 메모리 상에 IR 트리를 구축합니다.

2. 조작 (Compiler Plugin)

백엔드로 넘어가기 직전, Compose 같은 플러그인들이 이 IR 트리에 난입합니다.

트리의 가지를 자르거나(로직 삭제), 새로운 가지를 접붙이기(로직 추가)를 진행하여 코드를 수정합니다. 이 모든 건 파일이 아니라 메모리 위의 트리 구조에서 일어납니다.

3. 생성 (Backend)

조작이 끝난 최종 IR 트리를 백엔드가 받습니다. 백엔드는 트리를 순회하면서, 각 노드에 해당하는 기계어(바이트코드)를 파일로 써 내려갑니다.


어노테이션 프로세서 vs 코틀린 컴파일러 플러그인

앞서 언급했듯, 두 수단을 가르는 기준은 "컴파일러가 읽고 있는 설계도를 수정할 수 있는가?"입니다.

어노테이션 프로세서 (kapt & KSP)

이들은 기본적으로 '코드 생성기(Code Generator)'입니다. 기존 소스 코드를 읽고(Read-only), 그 내용을 바탕으로 새로운 소스 파일(.kt 또는 .java)을 만들어냅니다.

  • 동작 방식: 컴파일러가 소스 코드를 분석하는 '프론트엔드' 단계에서 활성화됩니다.
  • kapt: Java의 인프라를 빌려 쓰기 위해 Kotlin 코드를 Java용 가짜 코드(Stub)로 변환하는 과정을 거칩니다. 이 과정에서 병목 현상이 발생합니다.
  • KSP: Kotlin 컴파일러에 내장된 분석 엔진을 직접 활용하여 Stub 생성 없이 빠르게 심볼을 읽어냅니다.
  • 결정적 한계: "추가만 가능하고 수정은 불가능"합니다.

코틀린 컴파일러 플러그인

컴파일러 플러그인은 단순한 생성기를 넘어 '코드 변조기(Code Transformer)' 역할을 수행합니다.

  • 동작 방식: 컴파일러의 가장 깊은 곳인 IR(Intermediate Representation) 단계에 내장됩니다.
  • 특징: 프론트엔드가 분석을 마친 '설계도(IR)'를 직접 가로채서 노드를 삭제하거나, 로직을 끼워 넣거나, 구조를 완전히 변경합니다.
  • 대표 사례 - Compose
    • 우리가 @Composable 함수를 작성하면, 플러그인은 컴파일 타임에 이 함수의 파라미터에 $composer와 같은 상태 관리 객체를 강제로 주입합니다.
    • 소스 코드에는 직접 존재하지 않지만, 실제 실행되는 바이트코드에는 포함됩니다.

요약

코틀린 컴파일러는 텍스트가 아니라 코드를 의미 단위로 쪼갠 트리 구조(IR)를 통해 코드를 이해합니다.

IR은 메모리 상에서 프론트엔드(분석)와 백엔드(생성) 사이를 이어주며, 코틀린 플러그인은 소스코드가 아닌 IR 트리의 노드를 찾아 수정하는 방식으로 로직에 개입하여 수정할 수 있습니다.


참고 자료

profile
Software Engineer

0개의 댓글