모바일 환경에서 안드로이드 앱은 격리된 프로세스, 즉 샌드박스(Sandbox) 환경에서 동작한다.

이런 개별 프로세스에서 중요한 점은 단말의 한정된 메모리를 적절히 분배하는 것도 있지만, 앱 프로세스에 할당된 스레드(Thread)를 얼마나 적절히 나누느냐에 따라 UI/UX 측면에서 큰 차이가 발생한다.

코코 코코코코코루틴

할당된 스레드를 더 작은 단위인 태스크(Task)로 나눠 동작하게 만드는 기능이 바로 async와 코루틴(Coroutine)이다.

기존 RxKotlin 환경에서도 비동기 처리를 위해 async를 사용했지만, 최근 트렌드인 Compose에서는 코루틴을 더 적극적으로 활용하며 최적화되어 있다.

 

특히 Dispatcher를 통해 코루틴이 동작할 스코프를 지정할 수 있는데, 주요 설정은 다음과 같다.

  • Main: UI 스레드 (1개)
  • IO: 네트워킹 및 DB 작업 (최대 64개)
  • Default : JSON 파싱, 이미지 처리 (CPU 코어 수와 동일)

코루틴으로 실행된 함수는 실행 후 결과가 나오기까지 다른 함수에게 스레드 점유를 넘겨주고(Suspend), 결과가 나오면(await) 다시 태스크를 할당받아 결과를 전달해 주는 방식으로 동작한다.

 

CPS와 State Machine

 

이러한 코루틴의 '일시 중단(Suspend)'과 '재개(Resume)'가 가능한 이유는 컴파일 단계에서 CPS(Continuation Passing Style) 구조로 변환되기 때문이다. (컴파일 단계에서 factory 구조로 변환됨은 C를 실행했을 때, 컴파일러가 assembly 언어로 변경하는 것과 비슷하다. 이렇게 하면 보일러플레이트 코드 없이 컴파일러가 자동으로 실행까지 진행해준다.)

 

코루틴의 컴파일 단계에서 cps 변환 과정에 대한 flow

 

1. Continuation (코루틴의 Context)

 

OS가 스레드를 전환할 때 CPU 레지스터 상태를 저장하는 PCB(Process Control Block)나 Context를 사용하는 것처럼, 코루틴은 Continuation이라는 객체에 현재 작업 상태를 저장한다.

  • C <-> Assembly : 컨텍스트 스위칭 시 레지스터(EAX, ESP 등) 값을 커널 스택에 저장(Push)하고 복구(Pop)하는 하드웨어 의존적 비용이 발생한다.
  • Kotlin Coroutine: 컴파일러가 함수의 마지막 인자로 Continuation을 추가하며, 여기에 로컬 변수와 실행 지점(label)을 저장한다. 이는 커널의 개입 없이 Heap 영역에서 객체로 관리되므로 훨씬 가볍다.

2. State Machine: 컴파일러가 생성하는 Factory 코드

 

suspend 함수는 컴파일 타임에 일종의 State Machine 구조로 변경된다. 각 중단 지점(delay, await 등)을 기준으로 코드가 분절되며, switch-case 문과 유사한 형태로 변환된다.

  • 함수가 재개될 때마다 Continuation에 저장된 label 값을 확인하여 이전에 멈췄던 지점으로 즉시 점프(Jump)한다.
  • 이는 C언어에서 함수 호출 시 스택 프레임을 새로 생성하는 것과 달리, 하나의 객체 상태만 업데이트하며 로직을 이어가는 방식이다.

3. Summary: OS 스레드 vs 코루틴

결과적으로 코루틴은 OS가 인터럽트를 통해 강제로 CPU 점유를 뺏어오는 선점형(Preemptive) 방식이 아니라, 함수 스스로가 다음 타자에게 실행권을 양보하는 협력형(Cooperative) 모델이다.

 

android 코드의 실행 파일 변환 과정(jvm 위에서 동작하지만 .dex로 런타임 파일 생성 flow 추가)

+ Recent posts