withContext는
코루틴 안에서 다른 스레드나 디스패처로 잠시 전환해서 작업을 실행하는 함수이다.
📌 withContext의 동작 과정
- 현재 코루틴을 일시 중단한다.
- 새로운 디스패처(또는 스레드)로 컨텍스트를 전환한다. (예를 들어 Dispatchers.IO로 넘어가서 작업할 수 있다.)
- 람다 블록 안의 코드를 실행한다.
- 람다 블록의 실행이 완료되면, 그 결과를 반환하고
- 이전 코루틴 컨텍스트(원래 스레드)로 돌아간다.
→ 요약하면, "딴 데 가서 일하고, 끝나면 원래 자리로 돌아오는 것"이다.
📌 결과물
- 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가 내부에서 하는 일
- 현재 코루틴의 상태를 저장한다. (예: 어디까지 실행했는지, 어떤 변수값을 가졌는지)
- 요청한 디스패처(스레드 풀)로 컨텍스트를 교체한다. (예: Dispatchers.IO, Dispatchers.Default 등)
- 지정된 블록을 새로운 컨텍스트 안에서 실행한다.
- 블록이 끝나면, 다시 원래 코루틴 컨텍스트로 돌아온다.
- 그 후, withContext가 결과값을 반환하면서, 코루틴이 이어서 계속 진행된다.
📌 좀 더 구체적인 흐름
내부에서 일어나는 세부 과정은 이렇게 된다:
withContext(context) { block }
↓
코루틴이 suspend됨 (일시중단)
↓
context에 해당하는 스레드로 이동
↓
block을 실행
↓
block 실행이 끝나면 결과를 가지고 복귀
↓
원래 스레드(컨텍스트)로 돌아가서 나머지 코드를 이어서 실행
- 평소에 A 사무실에서 일하는데,
- 어떤 작업만 B 사무실로 가서 처리하고,
- 다 끝나면 다시 A 사무실로 돌아와서 일하는 것과 같다.
✅ 추가로 알아두면 좋은 것
- withContext는 새 코루틴을 만드는 게 아니라 기존 코루틴을 일시 중단해서 스레드를 바꾸는 것뿐이다.
- 컨텍스트 전환은 비용이 있기 때문에, 너무 자주 바꾸면 오히려 성능이 떨어질 수 있다.
- 실제로 내부 구현은 suspendCoroutine이나 ContinuationInterceptor를 사용해서 관리된다.
withContext의 코드를 직접 까서 Continuation이나 Dispatcher가 어떻게 돌아가는지도 살펴보자
📌 withContext의 내부 핵심 흐름
- withContext는 사실 suspend 함수다.
- 내부에서는 suspendCoroutineUninterceptedOrReturn 이라는 함수를 호출한다.
- 현재 코루틴의 Continuation(중단됐던 상태와 이어가기 정보를 담은 객체)을 가져온다.
- 새로 지정한 CoroutineContext를 Continuation에 적용한다.
- 새 Context의 Dispatcher가 "이 블록을 어느 스레드에서 실행할지" 결정한다.
- 블록이 끝나면 결과를 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가 스레드를 선택하는 과정까지 더 깊게 들어가보자