본문으로 건너뛰기

🎛️ Context & Dispatcher

📖 CoroutineContext란?

CoroutineContext는 코루틴이 어떻게 실행될지 결정하는 설정의 집합입니다. 어떤 쓰레드에서 실행될지, 이름은 무엇인지 등을 지정합니다!

💡 Dispatcher

기본 Dispatcher들

fun main() = runBlocking {
// Main - UI 쓰레드 (Android/Desktop)
launch(Dispatchers.Main) {
// UI 업데이트
}

// IO - 네트워크/파일 작업
launch(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
}

// Default - CPU 집약적 작업
launch(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
}

// Unconfined - 특별한 경우
launch(Dispatchers.Unconfined) {
println("Unconfined: ${Thread.currentThread().name}")
}

delay(100)
}

Dispatchers.IO

suspend fun readFile(): String = withContext(Dispatchers.IO) {
// 파일 읽기, 네트워크 요청 등
delay(1000)
"파일 내용"
}

suspend fun writeFile(content: String) = withContext(Dispatchers.IO) {
// 파일 쓰기
delay(500)
println("파일 저장: $content")
}

fun main() = runBlocking {
val content = readFile()
writeFile(content)
}

Dispatchers.Default

suspend fun heavyComputation(): Int = withContext(Dispatchers.Default) {
// CPU 집약적 계산
var result = 0
repeat(1_000_000) {
result += it
}
result
}

fun main() = runBlocking {
val result = heavyComputation()
println("계산 결과: $result")
}

🎯 실전 예제

레이어별 Dispatcher

// Repository - IO
class UserRepository {
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
delay(1000) // 네트워크 요청
User(id, "홍길동")
}
}

// UseCase - Default
class ProcessUserUseCase {
suspend fun process(user: User): ProcessedUser = withContext(Dispatchers.Default) {
// 데이터 처리
delay(500)
ProcessedUser(user.name.uppercase())
}
}

data class User(val id: String, val name: String)
data class ProcessedUser(val displayName: String)

fun main() = runBlocking {
val repo = UserRepository()
val useCase = ProcessUserUseCase()

val user = repo.fetchUser("123")
val processed = useCase.process(user)
println("결과: ${processed.displayName}")
}

병렬 IO 작업

suspend fun loadAllData(): Triple<String, String, String> = coroutineScope {
val user = async(Dispatchers.IO) {
delay(1000)
"사용자 데이터"
}

val posts = async(Dispatchers.IO) {
delay(1500)
"게시물 데이터"
}

val comments = async(Dispatchers.IO) {
delay(800)
"댓글 데이터"
}

Triple(user.await(), posts.await(), comments.await())
}

fun main() = runBlocking {
val time = measureTimeMillis {
val (user, posts, comments) = loadAllData()
println("$user, $posts, $comments")
}
println("소요 시간: ${time}ms") // ~1500ms (병렬)
}

🔧 Context 조합

이름 붙이기

fun main() = runBlocking {
launch(CoroutineName("작업1")) {
println("이름: ${coroutineContext[CoroutineName]}")
}

launch(Dispatchers.IO + CoroutineName("IO작업")) {
println("쓰레드: ${Thread.currentThread().name}")
println("이름: ${coroutineContext[CoroutineName]}")
}

delay(100)
}

Job 추가

fun main() = runBlocking {
val job = Job()

launch(job + Dispatchers.Default) {
println("작업 실행")
delay(1000)
println("작업 완료")
}

delay(500)
println("작업 취소")
job.cancel()
}

🎨 withContext

쓰레드 전환

suspend fun complexTask() {
println("시작: ${Thread.currentThread().name}")

// IO 작업
val data = withContext(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
"데이터"
}

// CPU 작업
val processed = withContext(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
data.uppercase()
}

println("끝: ${Thread.currentThread().name}")
println("결과: $processed")
}

fun main() = runBlocking {
complexTask()
}

최적화 패턴

// ❌ 불필요한 전환
suspend fun bad() {
withContext(Dispatchers.IO) {
val data1 = loadData1()
withContext(Dispatchers.Default) { // 불필요!
process(data1)
}
}
}

// ✅ 효율적
suspend fun good() {
val data1 = withContext(Dispatchers.IO) {
loadData1()
}

withContext(Dispatchers.Default) {
process(data1)
}
}

suspend fun loadData1() = delay(100)
suspend fun process(data: Unit) = delay(100)

🔥 실용 패턴

캐시 + 네트워크

class DataSource {
private var cache: String? = null

suspend fun getData(): String {
// 캐시 확인 (빠름)
cache?.let { return it }

// 네트워크 요청 (느림)
return withContext(Dispatchers.IO) {
delay(1000)
"새 데이터"
}.also { cache = it }
}
}

fun main() = runBlocking {
val source = DataSource()

// 첫 호출 - 네트워크
val time1 = measureTimeMillis {
println(source.getData())
}
println("첫 호출: ${time1}ms")

// 두 번째 - 캐시
val time2 = measureTimeMillis {
println(source.getData())
}
println("두 번째: ${time2}ms")
}

배치 처리

suspend fun processBatch(items: List<Int>): List<Int> {
return withContext(Dispatchers.Default) {
items.map { item ->
// 각 아이템 처리
item * 2
}
}
}

fun main() = runBlocking {
val items = List(100) { it }
val results = processBatch(items)
println("처리 완료: ${results.size}개")
}

타임아웃과 함께

suspend fun fetchWithTimeout(): String? {
return try {
withTimeout(2000) {
withContext(Dispatchers.IO) {
delay(3000) // 너무 오래 걸림
"데이터"
}
}
} catch (e: TimeoutCancellationException) {
null
}
}

fun main() = runBlocking {
val result = fetchWithTimeout()
println("결과: ${result ?: "타임아웃"}")
}

🛡️ 예외 처리

CoroutineExceptionHandler

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("에러 처리: ${exception.message}")
}

val job = launch(handler) {
throw Exception("문제 발생!")
}

job.join()
println("계속 실행")
}

SupervisorJob

fun main() = runBlocking {
val supervisor = SupervisorJob()

with(CoroutineScope(coroutineContext + supervisor)) {
val job1 = launch {
delay(500)
throw Exception("작업1 실패")
}

val job2 = launch {
delay(1000)
println("작업2 성공!")
}

try {
job1.join()
} catch (e: Exception) {
println("작업1 예외: ${e.message}")
}

job2.join()
}
}

🎯 커스텀 Dispatcher

쓰레드 풀 크기 지정

fun main() = runBlocking {
val customDispatcher = Dispatchers.IO.limitedParallelism(2)

repeat(5) { i ->
launch(customDispatcher) {
println("작업 $i: ${Thread.currentThread().name}")
delay(1000)
}
}

delay(3000)
}

단일 쓰레드

fun main() = runBlocking {
val singleThread = Dispatchers.Default.limitedParallelism(1)

repeat(3) { i ->
launch(singleThread) {
println("작업 $i: ${Thread.currentThread().name}")
delay(500)
}
}

delay(2000)
}

🤔 자주 묻는 질문

Q1. 어떤 Dispatcher를 써야 하나요?

A: 작업 종류에 따라 선택!

// IO - 네트워크, 파일, 데이터베이스
suspend fun fetchData() = withContext(Dispatchers.IO) { }

// Default - CPU 집약적 계산
suspend fun compute() = withContext(Dispatchers.Default) { }

// Main - UI 업데이트 (Android/Desktop)
suspend fun updateUI() = withContext(Dispatchers.Main) { }

Q2. withContext를 여러 번 써도 되나요?

A: 네! 필요할 때마다 전환하세요.

suspend fun workflow() {
val data = withContext(Dispatchers.IO) {
loadFromNetwork()
}

val processed = withContext(Dispatchers.Default) {
processData(data)
}

withContext(Dispatchers.Main) {
updateUI(processed)
}
}

Q3. Dispatcher를 지정안하면?

A: 부모 코루틴의 Context를 상속!

fun main() = runBlocking(Dispatchers.Default) {
launch { // Dispatchers.Default 상속
println(Thread.currentThread().name)
}
}

🎬 마치며

Context와 Dispatcher로 코루틴을 제어하세요!

핵심 정리:
✅ Dispatchers.IO - 네트워크/파일
✅ Dispatchers.Default - CPU 작업
✅ Dispatchers.Main - UI 업데이트
✅ withContext로 쓰레드 전환
✅ Context 조합으로 세밀한 제어

축하합니다! Coroutines 시리즈를 완료했습니다! 🎉

다음 단계: 단위 테스트에서 테스트 작성법을 배워보세요!