Jetpack Compose Internals - 3장. Compose 런타임 (1)

Compose 런타임의 핵심 데이터 구조인 슬롯 테이블과 변경 리스트를 파헤쳐 봅니다.

Imagem de capa

3장. Compose 런타임

2장에서 컴파일러가 Composable 함수를 어떻게 변형하는지 배웠다면, 3장에서는 변형된 함수들이 실행되는 환경인 Compose 런타임을 다룹니다.

런타임의 가장 핵심적인 두 데이터 구조는 슬롯 테이블(Slot Table)변경 리스트(Change List)입니다. 이 둘의 차이점을 이해하는 것이 Compose의 동작 원리를 파악하는 첫걸음입니다.


슬롯 테이블 vs 변경 리스트

많은 개발자들이 이 두 구조를 혼동하곤 합니다. 간단히 정의하면 다음과 같습니다:

리콤포저(Recomposer)는 이 과정을 조율하며, 언제 어떤 스레드에서 리컴포지션을 수행하고 변경 사항을 적용할지 결정합니다.

트리 구조 노드 트리의 기본 구조 - 루트(R)에서 자식 노드(A, B, C)로 연결됩니다.


슬롯 테이블 (Slot Table) 깊이 파헤치기

슬롯 테이블은 빠른 선형 액세스(linear access)를 위해 최적화되어 있습니다. 텍스트 에디터에서 흔히 쓰이는 갭 버퍼(Gap Buffer) 개념을 기반으로 하며, 두 개의 선형 배열로 데이터를 저장합니다.

// LinearStructures.kt (개념적 구조)
var groups = IntArray(0) // 그룹의 메타데이터 저장
var slots = Array<Any?>(0) { null } // 실제 데이터(값) 저장

그룹 (Groups) 배열

2장에서 컴파일러가 Composable 본문을 그룹으로 감싸는 것을 배웠습니다. 이 그룹들은 메모리에 저장될 때 고유한 키(identity)를 갖게 됩니다.

슬롯 (Slots) 배열

각 그룹에 해당하는 실제 데이터를 저장합니다. Any? 타입 배열로, remember된 값, 상태(State) 등 어떤 타입의 정보든 담을 수 있습니다. 각 그룹은 슬롯 배열의 특정 범위를 가리키며 자신의 데이터를 관리합니다.

갭 버퍼 (Gap Buffer) 방식

슬롯 테이블은 읽고 쓰기 위해 “갭(gap)”을 사용합니다. 갭은 현재 읽거나 쓰고 있는 위치를 나타내며, 데이터가 추가되거나 삭제될 때 이 갭이 이동하면서 배열 내의 데이터를 효율적으로 밀어내거나 덮어씁니다.

예를 들어 조건부 로직(if-else)이 있을 때:

SlotReader와 SlotWriter

안전한 데이터 접근을 위해 읽기 전용인 SlotReader와 쓰기 전용인 SlotWriter를 사용합니다.


변경 리스트 (Change List)

Composable 함수가 실행될 때 “방출(emit)”한다는 표현을 씁니다. 여기서 방출이란 슬롯 테이블을 업데이트하고 최종적인 노드 트리를 수정하기 위한 지연된 변경 사항(deferred changes)을 생성하는 것을 의미합니다.

왜 지연된 변경(Deferred Change)인가?

노드 트리를 직접 바로 수정하는 것은 비용이 많이 듭니다. 대신 Compose는:

  1. Composable 함수를 실행하며 현재 슬롯 테이블의 상태를 확인합니다.
  2. 이전 상태와 비교하여 무엇이 바뀌어야 하는지 계산합니다.
  3. 그 변경 작업(노드 이동, 삭제, 추가 등)을 리스트에 기록만 해둡니다.
  4. Composition 단계가 모두 끝나면, 이 리스트를 한꺼번에 실행하여 슬롯 테이블을 최신화하고 Applier를 통해 실제 UI 트리를 업데이트합니다.

이 방식 덕분에 “방출” 과정이 매우 빠르게 일어날 수 있습니다. 단순히 나중에 실행할 작업을 리스트에 담기만 하면 되기 때문입니다.


요약

다음 글에서는 Composer가 어떻게 이 두 구조를 활용하여 효율적인 UI 업데이트를 만들어내는지 더 자세히 알아보겠습니다.

이 시리즈는 “Jetpack Compose Internals” 책의 내용을 바탕으로 정리한 것입니다.