[coroutine] withContext의 동작 과정과 결과물을 알아보자

withContext는

코루틴 안에서 다른 스레드나 디스패처로 잠시 전환해서 작업을 실행하는 함수이다.


📌 withContext의 동작 과정

  1. 현재 코루틴을 일시 중단한다.
  2. 새로운 디스패처(또는 스레드)로 컨텍스트를 전환한다. (예를 들어 Dispatchers.IO로 넘어가서 작업할 수 있다.)
  3. 람다 블록 안의 코드를 실행한다.
  4. 람다 블록의 실행이 완료되면, 그 결과를 반환하고
  5. 이전 코루틴 컨텍스트(원래 스레드)로 돌아간다.

→ 요약하면, "딴 데 가서 일하고, 끝나면 원래 자리로 돌아오는 것"이다.


📌 결과물

  • withContext 블록의 마지막 표현식이 결과값이 된다.
  • 즉, withContext는 suspend 함수이며, 결과를 return한다.

예를 들어:

val result = withContext(Dispatchers.IO) {
    // IO 작업 수행
    readDataFromFile()
}
  • readDataFromFile()이 반환하는 값이 result에 들어간다.
  • 그리고 원래 코루틴이 실행되던 스레드로 돌아간다.

📌 예시

suspend fun fetchData(): String {
    val data = withContext(Dispatchers.IO) {
        // 무거운 IO 작업 (예: 파일 읽기, 네트워크 통신)
        "Data from IO"
    }
    return "Received: $data"
}
  • Dispatchers.IO에서 "Data from IO"라는 작업을 처리하고,
  • fetchData()는 "Received: Data from IO"를 반환한다.

 

일시중단 포인트는 어떻게 생기는걸까?
withContext의 내부 원리를 좀 더 깊게 살펴보자.


📌 코루틴은 어떻게 동작하는가?

  • 코루틴은 일반 함수처럼 즉시 실행되지 않고,
  • suspend 키워드 덕분에 중간에 멈췄다가, 필요할 때 다시 이어서 실행할 수 있다.
  • 이걸 "일시 중단(suspend) 지점"이라고 부른다.

withContext는 바로 이 일시 중단 지점을 만들어낸다.


📌 withContext가 내부에서 하는 일

  1. 현재 코루틴의 상태를 저장한다. (예: 어디까지 실행했는지, 어떤 변수값을 가졌는지)
  2. 요청한 디스패처(스레드 풀)로 컨텍스트를 교체한다. (예: Dispatchers.IO, Dispatchers.Default 등)
  3. 지정된 블록을 새로운 컨텍스트 안에서 실행한다.
  4. 블록이 끝나면, 다시 원래 코루틴 컨텍스트로 돌아온다.
  5. 그 후, withContext가 결과값을 반환하면서, 코루틴이 이어서 계속 진행된다.

📌 좀 더 구체적인 흐름

내부에서 일어나는 세부 과정은 이렇게 된다:

 

withContext(context) { block }

코루틴이 suspend됨 (일시중단)

context에 해당하는 스레드로 이동

block을 실행

block 실행이 끝나면 결과를 가지고 복귀

원래 스레드(컨텍스트)로 돌아가서 나머지 코드를 이어서 실행

 

  • 평소에 A 사무실에서 일하는데,
  • 어떤 작업만 B 사무실로 가서 처리하고,
  • 다 끝나면 다시 A 사무실로 돌아와서 일하는 것과 같다.

✅ 추가로 알아두면 좋은 것

  • withContext는 새 코루틴을 만드는 게 아니라 기존 코루틴을 일시 중단해서 스레드를 바꾸는 것뿐이다.
  • 컨텍스트 전환은 비용이 있기 때문에, 너무 자주 바꾸면 오히려 성능이 떨어질 수 있다.
  • 실제로 내부 구현은 suspendCoroutine이나 ContinuationInterceptor를 사용해서 관리된다.

withContext의 코드를 직접 까서 Continuation이나 Dispatcher가 어떻게 돌아가는지도 살펴보자


📌 withContext의 내부 핵심 흐름

  1. withContext는 사실 suspend 함수다.
  2. 내부에서는 suspendCoroutineUninterceptedOrReturn 이라는 함수를 호출한다.
  3. 현재 코루틴의 Continuation(중단됐던 상태와 이어가기 정보를 담은 객체)을 가져온다.
  4. 새로 지정한 CoroutineContext를 Continuation에 적용한다.
  5. 새 Context의 Dispatcher가 "이 블록을 어느 스레드에서 실행할지" 결정한다.
  6. 블록이 끝나면 결과를 resume해서 코루틴을 다시 이어준다.

📌 한 줄로 요약

Continuation을 새 Context로 "옮기고", 블록을 실행한 뒤, 끝나면 다시 이어붙여서 코루틴을 재개한다.


📌 주요 코드 흐름 (개념화해서)

suspend fun <T> withContext(context: CoroutineContext, block: suspend () -> T): T {
    // 현재 코루틴의 Continuation을 저장
    val currentContinuation = currentContinuation()

    // context를 바꾼 Continuation을 만든다
    val newContinuation = currentContinuation.withNewContext(context)

    // Dispatcher가 "어떤 스레드에서 실행할지" 정해서 block을 실행
    dispatcher.schedule(newContinuation) {
        block() // 이 블록 안에서 우리가 쓴 코드가 실행됨
    }

    // 결과를 받아서 코루틴을 재개
}

(주의) 실제 구현은 더 복잡하고 최적화되어 있지만, 핵심 개념은 이렇다.


📌 조금 더 깊은 개념어 설명

용어 뜻

Continuation 코루틴이 "멈춘 지점"을 기억하는 객체. 이어서 실행할 때 쓴다.
suspendCoroutineUninterceptedOrReturn 코루틴을 멈추고, 나중에 다시 이어주겠다고 약속하는 저수준 함수.
Dispatcher 스레드나 큐를 관리하는 책임자. 어떤 스레드에서 block을 실행할지 정한다.
resume Continuation을 "이어붙여서" 다시 실행하는 것.

📌 비유로 다시 풀어주면

  • 책을 읽다가 "책갈피"를 꽂고(Continuation 저장),
  • 다른 방(새 Context)으로 이동해서,
  • 거기서 책을 계속 읽는 것(block 실행),
  • 다 읽으면 책을 원래 방으로 가져와서 다시 읽기 시작하는 것(resume).

✅ 요약

  • withContext = Context(스레드) 전환 + 블록 실행 + 다시 이어서 코루틴 재개
  • Continuation 조작이 핵심 기술
  • Dispatcher가 스레드를 골라준다
  • 결과적으로 코루틴은 "멈추고 → 다른 컨텍스트에서 실행하고 → 다시 이어붙인다"

Continuation 객체가 어떤 구조인지, 실제로 Dispatcher가 스레드를 선택하는 과정까지 더 깊게 들어가보자