컴포즈의 아키텍처는 개발자가 작성한 소스 코드(Sources)가 컴파일러(Compiler)를 통해 런타임(Runtime)이 이해할 수 있는 코드로 변환되고, 이 런타임이 최종적으로 UI를 그리거나 업데이트하는 흐름으로 구성된다.

@Composable 어노테이션을 사용하여 작성한 Kotlin 소스 코드.compose-ui)| 구성 요소 | 역할 | 비고 |
|---|---|---|
| Compose Compiler | Composable 함수를 분석하고 런타임이 요구하는 코드로 변환하는 역할. | Kotlin 컴파일러 플러그인 형태로 동작함. |
| Compose Runtime | Composable 트리의 인메모리 표현을 관리하고 Recomposition을 트리거함. | UI에 종속되지 않은 포괄적 디자인을 가짐. |
| Compose UI | Compiler와 Runtime을 활용하는 안드로이드 UI 툴킷 클라이언트. | Compose Multiplatform 등 다른 클라이언트 존재. |
기존 안드로이드 생태계에서 코드 생성에 널리 사용되던 kapt와 같은 어노테이션 프로세서와 달리, 컴포즈는 'Kotlin 컴파일러 플러그인'이라는 방식을 채택함. 이는 단순히 새로운 코드를 추가하는 수준을 넘어, 컴파일 과정에 직접 개입하여 기존 코드의 구조를 변형하고 언어의 기능을 확장하는 강력한 접근 방식임.
컴파일러 플러그인 방식은 컴파일 과정에 직접 통합되어 더 깊은 수준의 코드 접근과 빠른 피드백을 제공함.
| 구분 | 어노테이션 프로세서 (kapt) | Kotlin 컴파일러 플러그인 (Compose) |
|---|---|---|
| 실행 시점 | 컴파일 이전 단계에서 실행됨. | Kotlin 컴파일 과정에 직접 내장되어 함께 실행됨. |
| 코드 접근/수정 | 새로운 코드 추가만 가능. 기존 코드 수정은 불가능함. | 기존 코드의 IR을 정적 분석하고 직접 수정 및 변형 가능. |
| 피드백 속도 | 상대적으로 느림. | 컴파일러 프론트엔드 단계에서 진단하여 컴파일 시 피드백 루프가 매우 빠름. |
| 핵심 장점 | 표준화된 코드 생성 방식. | 언어 자체를 확장하는 수준의 강력한 코드 변형과 최적화 가능. |
컴포즈 컴파일러 플러그인이 제공하는 빠른 진단은 컴파일 시점에 이루어짐. 우리가 코드를 작성하는 동안 IDE에서 즉각적으로 받는 경고나 오류는 컴파일러 플러그인이 직접 보고하는 것이 아니라, 별도의 전용 IDEA 플러그인을 통해 제공됨.
컴포즈에서 어노테이션은 단순한 메타데이터를 넘어, 컴파일러의 동작을 유도하고 제어하는 핵심적인 '계약(Contract)'으로 작용. 개발자가 이 계약을 준수하면, 컴파일러의 가장 강력한 최적화 기능들을 활성화할 수 있음. 각 어노테이션이 컴파일러에게 어떤 정보를 제공하고, 이를 통해 어떤 최적화나 제약이 발생하는지 분석하는 것이 중요함.
@Composable@Composable 어노테이션은 다음 주요 기능을 갖음.
remember를 호출하고 내부적으로 Composer를 활용할 수 있는 능력을 부여함. 이를 통해 상태를 기억하고 UI 트리를 구축할 수 있게 됨.LaunchedEffect와 같은 이펙트(effects)들이 Recomposition 과정에서도 자신의 상태를 유지하고 올바르게 동작할 수 있는 생명주기를 제공함.@DisallowComposableCallsremember 함수의 calculation 람다로, 이 람다는 최초 Composition 시에만 실행되어야 하므로 내부에서 UI 노드를 방출하는 Composable 함수 호출이 허용되지 않음. 컴파일러는 이 어노테이션을 보고 해당 람다 내에서의 Composable 호출을 컴파일 오류로 진단함.
@ReadOnlyComposableCompositionLocal 값 조회)만 수행함을 컴파일러에 알리는 최적화 어노테이션임. 컴파일러는 이 약속을 믿고, 해당 함수를 추적하거나 재정렬할 필요가 없다고 판단하여 재시작(restartable)이나 이동(movable) 가능한 컨트롤 플로우 그룹의 생성을 생략함.MaterialTheme.colors나 LocalContext.current와 같이 한 번 설정된 후 변경되지 않는 값을 조회하는 유틸리티 함수들이 대표적인 예시임.@NonRestartableComposable타입 안정성은 입력값이 변경되지 않았을 때 Composable 함수의 재실행을 건너뛰는 '스마트 Recomposition'의 핵심 전제 조건임. 아래 어노테이션들은 특정 타입이 안정적이라는 것을 컴파일러에 약속하여 최적화를 유도함.
@StableMarker @Immutable과 @Stable의 기반이 되는 메타 어노테이션으로, 안정성을 위해 타입이 충족해야 할 세 가지 요구사항을 정의함.equals 함수의 결과는 항상 동일해야 함.@Immutable 클래스의 인스턴스가 생성된 후에는 어떠한 public 프로퍼티도 절대 변경되지 않음을 보장하는 강력한 약속임. 이는 프로퍼티 재할당을 막는 Kotlin의 val보다 더 엄격한 개념으로, val로 선언된 가변 컬렉션(e.g., MutableList)의 내부 요소 변경까지 금지하는 수준의 불변성을 의미함. 컴파일러는 이 약속을 바탕으로 해당 타입의 객체 비교를 생략할 수 있음.@Stable 이 어노테이션은 적용 대상에 따라 두 가지 의미를 가짐.MutableState가 대표적인 예로, 내부 값은 변경될 수 있지만 그 변경이 Composition 시스템에 통지되므로 안정적으로 간주됨.코드 생성에 앞서, 컴포즈 컴파일러는 코드의 유효성을 검사하고 개발자에게 빠른 피드백을 제공하는 '정적 분석' 단계를 수행함. 이 단계는 Kotlin 컴파일러의 프론트엔드에서 실행되므로, 무거운 컴파일 작업을 거치지 않고도 빠른 피드백 루프를 가능하게 하여 개발 생산성을 크게 향상시키는 중요한 과정임.
컴파일러는 라이브러리의 올바른 사용을 보장하기 위해 다양한 정적 검사기를 등록하여 코드의 문법과 규칙을 검증함.
@Composable 함수가 허용되지 않는 컨텍스트에서 호출되는지를 검사함. 예를 들어, @DisallowComposableCalls 어노테이션이 붙은 람다 내부나, try/catch 블록과 같이 컴포즈 런타임이 상태를 올바르게 추적할 수 없는 곳에서 Composable 함수를 호출하면 오류를 보고함. 컴파일러는 PSI(Program Structure Interface) 트리를 순회하며 현재 코드의 컨텍스트를 추적하고 규칙 위반 여부를 판단함.@Composable 어노테이션이 함수 타입의 일부로 올바르게 사용되었는지 검사함. 예를 들어, @Composable () -> Unit 타입의 매개변수가 필요한 곳에 일반 () -> Unit 타입의 람다가 전달되면, 컴파일러는 타입 불일치 오류를 보고하고 @Composable 어노테이션을 추가하도록 제안함.@Composable 어노테이션이 지원되지 않는 선언과 함께 사용되는 것을 금지함. 예를 들어, Composable 함수는 suspend 함수가 될 수 없으며, 프로그램의 진입점인 main 함수 역시 Composable로 선언할 수 없음. 이러한 규칙들은 컴파일러가 각 선언의 유효성을 검사하여 강제함.컴포즈 컴파일러는 때로는 일반적인 Kotlin 문법에서는 오류로 간주될 상황을 의도적으로 '무시'하여 컴포즈만의 유연한 문법을 허용함. 이를 '진단 제지기'라고 하며, 컴파일러 플러그인의 강력함을 보여주는 대표적인 사례임.
@Composable 어노테이션에 한해 이 제약을 우회하여, 개발자가 인라인 함수의 람다 매개변수를 호출하는 위치에서 @Composable을 붙일 수 있도록 지원함.(String) -> Unit). 그러나 컴포즈 컴파일러는 @Composable 함수 타입에 한해 이 규칙을 제지하여 content: @Composable (path: String) -> Unit 와 같은 선언을 가능하게 함. 이는 코드 가독성을 크게 향상시킴.정적 분석을 통해 코드의 유효성을 확보한 컴파일러는, 이제 코드를 실질적으로 변형하고 런타임에 필요한 정보를 심는 '코드 생성' 단계로 넘어감.
컴포즈 컴파일러의 가장 핵심적인 역할은 개발자가 작성한 선언적 코드를 런타임이 실제로 실행할 수 있는 구체적인 코드로 변환하는 '코드 생성'임. 이 과정은 컴파일러 백엔드에서 소스 코드의 IR(중간 표현)을 직접 수정하고 변형하는 방식으로 이루어짐. 이처럼 더 높은 수준의 추상적인 개념을 더 낮은 수준의 구체적인 코드로 변환하는 과정을 '낮추기(Lowering)'라고 부름. 컴파일러는 이 단계를 통해 런타임에 필요한 암시적 매개변수를 주입하고, 최적화를 위한 그룹을 생성하며, Recomposition을 효율적으로 처리하기 위한 장치들을 코드에 심어 넣음.
@Stable 또는 @Immutable 어노테이션이 없더라도, 클래스의 모든 필드와 제네릭 타입을 분석하여 안정성을 자동으로 추론함. 이 추론 결과를 바탕으로 런타임에 안정성 정보를 제공하기 위해 $stable이라는 합성 필드를 클래스에 추가함. 하지만 모든 타입을 완벽하게 추론할 수는 없음.List<T>와 같은 인터페이스 타입은 불안정(unstable)하다고 간주됨. 컴파일러는 해당 인터페이스가 ArrayList와 같은 가변적인 구현체에 의해 인스턴스화될 가능성을 배제할 수 없기 때문임.class Foo<T>(val value: T)와 같은 제네릭 클래스의 안정성은 런타임에 T에 전달되는 실제 타입의 안정성에 따라 결정됨. 컴파일러는 이 의존성을 코드에 기록하여 런타임이 최종 안정성을 판단할 수 있도록 함.remember 호출 코드를 생성하여 람다 인스턴스를 감싸줌. 이를 통해 입력값이 동일할 경우 기존 람다 인스턴스를 재사용함.composableLambda라는 팩토리 함수 호출로 람다를 래핑함. 이 팩토리 함수는 고유 키를 사용하여 람다를 슬롯 테이블에 저장하고 재사용함. 이 방식은 "도넛 홀 생략하기(donut-hole skipping)"라는 강력한 최적화를 가능하게 함. 즉, 상위 계층에서 람다 인스턴스가 변경되더라도, 그 람다를 실제로 호출(읽기)하는 하위 Composable만 Recomposition되고, 중간에 람다를 그저 전달만 하는 Composable들은 건너뛸 수 있게 됨.@Composable 함수에 $composer라는 추가 매개변수를 암시적으로 주입함. 이 Composer 객체는 런타임의 슬롯 테이블과 상호작용하며 UI 트리를 구축하고 업데이트하는 모든 작업을 관장함. 개발자는 이 매개변수를 직접 다루지 않지만, 컴파일러에 의해 모든 Composable 호출에 자동으로 전달됨.$changed라는 비트마스크(bitmask) 매개변수를 모든 Composable 함수에 추가함. 이 비트마스크는 단순히 '변경됨/안됨'을 넘어 더 세밀한 정보를 인코딩하여 런타임이 불필요한 equals() 비교를 생략하게 함.equals()를 호출하여 이전 값과 비교함.$default라는 비트마스크 매개변수를 추가하여 어떤 매개변수가 호출자로부터 전달되지 않아 디폴트 값을 사용해야 하는지를 추적함. 이는 디폴트 값을 계산하는 표현식이 Composable 함수의 실행 컨텍스트(그룹) 내에서 실행되어야 하기 때문이며, 컴포즈만의 방식으로 이를 구현한 것임.if, for, when과 같은 제어 흐름 구문을 런타임이 인식하고 관리할 수 있도록, 컴파일러는 해당 코드 블록을 특정 목적을 가진 '그룹'으로 감쌈.if-else 문과 같이 조건에 따라 UI의 일부가 다른 것으로 교체될 때 사용됨. 런타임은 이 그룹을 통해 이전 분기(branch)의 노드를 정리하고 새로운 분기의 노드를 생성함.key 함수와 함께 사용되어 리스트 아이템의 순서가 변경되었을 때 UI 노드를 파괴하고 새로 만드는 대신, 기존 노드를 효율적으로 재정렬(이동)할 수 있게 함.State)를 읽는 모든 Composable 함수를 감싸는 가장 중요한 그룹임. 상태 값이 변경되었을 때, 전체 UI를 재구성하는 것이 아니라 이 그룹으로 감싸진 Composable만 독립적으로 Recomposition('재시작') 될 수 있도록 하는 스코프(scope)를 제공함. 이는 컴포즈의 효율적인 Recomposition의 핵심 원리임.Jetpack Compose 컴파일러는 단순히 Kotlin 코드를 바이트코드로 변환하는 도구가 아님. 이는 개발 과정에 능동적으로 참여하는 파트너와 같음.