본문 바로가기
language/Kotilin In Action

7주차(14,15)

by abstract.jiin 2025. 9. 1.

14장 코루틴


14.4-14.6 코루틴

왜 코루틴이 필요할까?

문제: 블로킹 코드의 한계

*// 기존 방식 - 스레드를 블록시킴* 
fun showUserInfo(credentials: Credentials) {
    val userID = login(credentials)        *// 네트워크 요청 (2초 대기)*
    val userData = loadUserData(userID)    *// 네트워크 요청 (3초 대기)*
    showData(userData)                     *// 화면에 표시*
}

*// 문제점:
// 1. 총 5초 동안 스레드가 멈춤
// 2. UI가 있다면 화면이 얼어버림
// 3. 서버라면 다른 요청을 처리할 수 없음*

해결: suspend 함수 사용

*// 코루틴 방식 - 스레드를 블록시키지 않음*
suspend fun showUserInfo(credentials: Credentials) {
    val userID = login(credentials)        *// 대기 중에 다른 작업 가능!*
    val userData = loadUserData(userID)    *// 대기 중에 다른 작업 가능!*
    showData(userData)
}

*// 장점:
// 1. 코드는 순차적으로 보임 (읽기 쉬움)
// 2. 대기 중에 스레드가 다른 일을 할 수 있음
// 3. UI가 멈추지 않음, 서버가 다른 요청도 처리 가능*

suspend 함수 - 일시 중단의 마법

suspend의 의미

suspend fun login(credentials: Credentials): UserID {
    *// 네트워크 요청 중...*
    delay(2000) *// 2초 대기 (다른 작업에게 스레드 양보)*
    return UserID("user123")
}

*// suspend = "잠시 멈출 수 있는 함수"
// 멈추는 동안 스레드는 다른 일을 할 수 있음!*

일반 함수 vs suspend 함수

*// 일반 함수 - 블로킹*
fun normalWork(): String {
    Thread.sleep(1000)  *// 스레드가 1초 동안 멈춤*
    return "완료"
}

*// suspend 함수 - 논블로킹*  
suspend fun suspendWork(): String {
    delay(1000)  *// 다른 작업에게 스레드 양보* 
    return "완료"
}

코루틴 빌더들

1. runBlocking - 블로킹 세계와 코루틴 세계 연결

fun main() = runBlocking {  *// 메인 함수를 코루틴으로!*
    println("코루틴 시작")
    delay(1000)
    println("1초 후 완료")
}

*// runBlocking의 특징:
// - 내부의 모든 코루틴이 끝날 때까지 기다림
// - 주로 main 함수나 테스트에서 사용
// - 하나의 스레드는 블록하지만, 내부에서는 여러 코루틴 실행 가능*

2. launch - 발사 후 잊기 (Fire and Forget)

fun main() = runBlocking {
    println("메인 코루틴 시작")

    *// 첫 번째 자식 코루틴*
    launch {
        delay(100)
        println("첫 번째 작업 완료")
    }

    *// 두 번째 자식 코루틴*  
    launch {
        delay(200)
        println("두 번째 작업 완료")
    }

    println("두 작업 시작함")
    delay(300) *// 자식들이 끝날 때까지 기다림*
}

*// 출력:
// 메인 코루틴 시작
// 두 작업 시작함
// 첫 번째 작업 완료 (100ms 후)
// 두 번째 작업 완료 (200ms 후)*

3. async - 결과값을 반환하는 비동기 작업

그러나 async 함수의 반환
타입은 launch와 달리 Deferred 인스턴스다. Deferred를 사용해 주로 할 일은
await라는 일시 중단 함수로 그 결과를 기다리는 것이다

await을 호출하면 그 Deferred에서 결괏값이 사용 가능해질 때까지
루트 코루틴이 일시 중단된다

suspend fun calculateSomething(num: Int): Int {
    delay(100 * num)  *// num * 100ms 대기*
    return num * num
}

fun main() = runBlocking {
    println("계산 시작")

    *// 두 계산을 동시에 시작*
    val result1 = async { calculateSomething(2) }  *// 4 (200ms 후)*
    val result2 = async { calculateSomething(3) }  *// 9 (300ms 후)*

    println("계산 중...")

    *// 결과를 기다림*
    println("첫 번째 결과: ${result1.await()}")  
    println("두 번째 결과: ${result2.await()}")
}

*// 총 시간: 300ms (더 긴 계산 시간)
// 순차 실행이었다면: 500ms (200ms + 300ms)*

다른 방식들과 비교

1. 콜백 지옥

*// 콜백 방식 - 복잡하고 읽기 어려움*
fun showUserInfo(credentials: Credentials) {
    loginAsync(credentials) { userID ->
        loadUserDataAsync(userID) { userData ->
            showData(userData)
            *// 더 많은 작업이 있다면...
            // 콜백 안에 또 콜백 안에 또 콜백...* 
        }
    }
}

2. Future/CompletableFuture

*// Future 방식 - 새로운 연산자 학습 필요*
fun showUserInfo(credentials: Credentials) {
    loginAsync(credentials)
        .thenCompose { loadUserDataAsync(it) }
        .thenAccept { showData(it) }
    *// thenCompose, thenAccept 등 새로운 개념 필요*
}

3. 코루틴 방식

*// 코루틴 방식 - 순차적이고 직관적*
suspend fun showUserInfo(credentials: Credentials) {
    val userID = login(credentials)        *// 자연스러운 순서*
    val userData = loadUserData(userID)    *// 읽기 쉬움*
    showData(userData)                     *// 마치 동기 코드 같음*
}

실제 동작 과정

여러 코루틴이 하나의 스레드에서 실행

fun main() = runBlocking {
    log("부모 코루틴 시작")

    launch {
        log("자식 1 시작")
        delay(100)  *// 100ms 대기 (다른 코루틴에게 양보)*
        log("자식 1 완료")
    }

    launch {
        log("자식 2 시작")
        log("자식 2 완료")  *// 즉시 완료*
    }

    log("부모 코루틴이 자식들 시작함")
}

*// 출력 (모두 같은 main 스레드에서 실행):
// [main] 부모 코루틴 시작
// [main] 부모 코루틴이 자식들 시작함
// [main] 자식 1 시작
// [main] 자식 2 시작
// [main] 자식 2 완료
// [main] 자식 1 완료 (100ms 후)*

시간 흐름 시각화

시간 →  0ms    50ms   100ms  150ms
       ┌─────┬─────┬─────┬─────┐
main   │부모  │자식2│기다림│자식1│
스레드 │코루틴│실행 │     │재개 │
       └─────┴─────┴─────┴─────┘
              ↑           ↑
           자식1이      자식1이
           일시중단      재개됨

코루틴 빌더 선택 가이드

*// runBlocking: main 함수나 테스트에서*
fun main() = runBlocking {
    *// 코루틴 세계의 시작점*
}

*// launch: 결과가 필요 없는 작업*
launch {
    saveToDatabase(data)  *// 저장하고 끝*
    sendEmail(user)       *// 이메일 보내고 끝*
}

*// async: 결과가 필요한 작업*
val userProfile = async { fetchUserProfile(userId) }
val userPosts = async { fetchUserPosts(userId) }

*// 두 결과를 모두 사용*
val combinedData = CombinedData(
    profile = userProfile.await(),
    posts = userPosts.await()
)

suspend 함수 호출 규칙

어디서 호출할 수 있나?

suspend fun mySuspendFunction() {
    delay(1000)
}

*// 일반 함수에서는 호출 불가*
fun normalFunction() {
    mySuspendFunction()  *// 컴파일 에러!*
}

*// suspend 함수에서는 호출 가능*
suspend fun anotherSuspendFunction() {
    mySuspendFunction()  *// OK!*
}

*// 코루틴 빌더 안에서는 호출 가능*
fun main() = runBlocking {
    mySuspendFunction()  *// OK!*
}

핵심 정리

코루틴의 3대 장점

  1. 순차적 코드: 읽기 쉽고 직관적
  2. 논블로킹: 스레드를 효율적으로 사용
  3. 간단한 문법: suspend 키워드만 추가

언제 뭘 쓸까?

  • Main 함수: runBlocking
  • 로그 저장, 이메일 발송: launch
  • API 호출, 계산: async + await
  • 순차 작업: 그냥 suspend fun

14.7-14.8 디스패처와 코루틴 컨텍스트 - 실행 환경 제어

디스패처 - 코루틴을 어디서 실행할지 결정

디스패처의 역할

*// 디스패처 = "이 코루틴을 어떤 스레드에서 실행할지 결정하는 매니저"
// 기본 디스패처 - CPU 집약적 작업*
launch(Dispatchers.Default) {
    *// 복잡한 계산 작업*
}

*// IO 디스패처 - 네트워크, 파일 작업*  
launch(Dispatchers.IO) {
    *// 파일 읽기, API 호출*
}

*// 메인 디스패처 - UI 업데이트*
launch(Dispatchers.Main) {
    *// 화면 업데이트*
}

3가지 주요 디스패처

1. Dispatchers.Default - 범용 작업

*// CPU 코어 수만큼의 스레드 풀 사용
// 계산 집약적인 작업에 적합*

launch(Dispatchers.Default) {
    *// 복잡한 수학 계산*
    val result = (1..1000000).map { it * it }.sum()

    *// 이미지 처리*
    processLargeImage(image)

    *// 데이터 정렬*
    largeList.sortedWith(complexComparator)
}

*// 언제 사용?
// - 기본적인 모든 코루틴 작업
// - CPU를 많이 사용하는 계산
// - 별다른 제약이 없는 일반적인 작업*

2. Dispatchers.IO - 입출력 작업

*// 많은 스레드 풀 사용 (최대 64개 + CPU 코어 수)
// 블로킹 API 호출에 적합*

launch(Dispatchers.IO) {
    *// 파일 읽기/쓰기*
    val content = File("large-file.txt").readText()

    *// 네트워크 API 호출*
    val response = httpClient.get("https://api.example.com/data")

    *// 데이터베이스 작업*
    val users = database.getAllUsers()
}

*// 언제 사용?
// - 파일 시스템 작업
// - 네트워크 요청
// - 데이터베이스 쿼리
// - 블로킹 API 호출*

3. Dispatchers.Main - UI 작업

*// UI 스레드에서 실행 (안드로이드, JavaFX, Swing 등)// 화면 업데이트 전용*

launch(Dispatchers.Main) {
    *// 안드로이드*
    textView.text = "업데이트된 텍스트"
    progressBar.visibility = View.GONE

    *// JavaFX*
    label.text = "완료!"

    *// Swing*
    jLabel.setText("작업 완료")
}

*// 언제 사용?
// - UI 요소 업데이트
// - 사용자 인터페이스 변경
// - UI 스레드에서만 가능한 작업*

withContext - 디스패처 전환

전형적인 UI 패턴

*// UI 애플리케이션의 고전적 패턴*
launch(Dispatchers.Main) {  *// UI 스레드에서 시작*
    showLoading(true)  *// 로딩 표시*

    val result = withContext(Dispatchers.IO) {  *// IO 스레드로 전환// 백그라운드에서 네트워크 작업*
        downloadLargeFile()
    }  *// 다시 Main 스레드로 돌아옴*

    showLoading(false)  *// 로딩 숨김*
    displayResult(result)  *// 결과 화면에 표시*
}

복잡한 작업 분할

suspend fun processUserData(userId: String) {
    *// 1. DB에서 사용자 정보 가져오기 (IO 작업)*
    val userInfo = withContext(Dispatchers.IO) {
        database.getUser(userId)
    }

    *// 2. 복잡한 계산 수행 (CPU 집약적)*
    val processedData = withContext(Dispatchers.Default) {
        performComplexCalculation(userInfo)
    }

    *// 3. 결과를 파일에 저장 (IO 작업)*
    withContext(Dispatchers.IO) {
        saveToFile(processedData)
    }

    *// 4. UI 업데이트 (Main 스레드)*
    withContext(Dispatchers.Main) {
        updateUI("처리 완료")
    }
}

스레드 안전성 주의사항

안전한 경우 - 단일 코루틴

fun safeExample() = runBlocking {
    launch(Dispatchers.Default) {
        var counter = 0
        repeat(10_000) {
            counter++  *// 안전! 하나의 코루틴에서 순차 실행*
        }
        println("결과: $counter")  *// 10000 (정확함)*
    }
}

위험한 경우 - 여러 코루틴이 같은 데이터 수정

fun dangerousExample() = runBlocking {
    var counter = 0

    repeat(10_000) {
        launch(Dispatchers.Default) {
            counter++  *// 위험! 여러 코루틴이 동시에 수정*
        }
    }

    delay(1000)
    println("결과: $counter")  *// 9916 (예상보다 적음)*
}

해결책 1: Mutex 사용

fun safeMutexExample() = runBlocking {
    val mutex = Mutex()
    var counter = 0

    repeat(10_000) {
        launch(Dispatchers.Default) {
            mutex.withLock {  *// 한 번에 하나씩만 실행*
                counter++
            }
        }
    }

    delay(1000)
    println("결과: $counter")  *// 10000 (정확함)*
}

해결책 2: 원자적 데이터 구조 사용

import java.util.concurrent.atomic.AtomicInteger

fun safeAtomicExample() = runBlocking {
    val counter = AtomicInteger(0)

    repeat(10_000) {
        launch(Dispatchers.Default) {
            counter.incrementAndGet()  *// 원자적 연산*
        }
    }

    delay(1000)
    println("결과: ${counter.get()}")  *// 10000 (정확함)*
}

코루틴 컨텍스트 - 코루틴의 신분증

컨텍스트가 뭔가요?

*// CoroutineContext = 코루틴에 대한 정보를 담는 집합
// - 디스패처: 어떤 스레드에서 실행할지
// - Job: 코루틴의 생명주기 관리
// - 이름: 디버깅용 이름
// - 예외 핸들러: 예외 처리 방법*

suspend fun checkContext() {
    println("현재 컨텍스트: $coroutineContext")
}

컨텍스트 조합하기

fun main() = runBlocking {
    *// 여러 요소를 + 연산자로 조합*
    launch(
        Dispatchers.IO +                    *// 디스패처*
        CoroutineName("MyCoroutine") +      *// 이름*
        SupervisorJob()                     *// 특별한 Job*
    ) {
        println("이름: ${coroutineContext[CoroutineName]}")
        println("디스패처: ${coroutineContext[ContinuationInterceptor]}")
    }
}

실무 패턴들

1. 안드로이드 MVVM 패턴

class UserViewModel : ViewModel() {
    fun loadUserData(userId: String) {
        viewModelScope.launch {  *// Main 디스패처에서 시작*
            _loading.value = true

            try {
                *// 백그라운드에서 데이터 로드*
                val userData = withContext(Dispatchers.IO) {
                    userRepository.getUser(userId)
                }

                *// 메인 스레드에서 UI 업데이트*
                _userData.value = userData
            } catch (e: Exception) {
                _error.value = e.message
            } finally {
                _loading.value = false
            }
        }
    }
}

2. 서버 API 처리

class UserService {
    suspend fun processUserRequest(request: UserRequest): UserResponse {
        *// 1. 입력 검증 (빠른 작업, Default)*
        val validatedData = withContext(Dispatchers.Default) {
            validateUserInput(request)
        }

        *// 2. 데이터베이스 조회 (IO 작업)*
        val userData = withContext(Dispatchers.IO) {
            database.findUser(validatedData.userId)
        }

        *// 3. 비즈니스 로직 처리 (계산 집약적)*
        val processedData = withContext(Dispatchers.Default) {
            processBusinessLogic(userData)
        }

        *// 4. 결과 저장 (IO 작업)*
        withContext(Dispatchers.IO) {
            database.saveProcessedData(processedData)
        }

        return UserResponse(processedData)
    }
}

3. 파일 처리 시스템

class FileProcessor {
    suspend fun processLargeFile(filePath: String) {
        *// 파일 읽기 (IO)*
        val content = withContext(Dispatchers.IO) {
            File(filePath).readText()
        }

        *// 데이터 처리 (CPU 집약적)*
        val processedData = withContext(Dispatchers.Default) {
            content.lines()
                .filter { it.isNotBlank() }
                .map { processLine(it) }
                .sorted()
        }

        *// 결과 저장 (IO)*
        withContext(Dispatchers.IO) {
            File("${filePath}.processed").writeText(
                processedData.joinToString("\n")
            )
        }
    }
}

핵심 정리

디스패처 선택 가이드

*// 어떤 작업인가?
// CPU 계산 → Dispatchers.Default*
launch(Dispatchers.Default) {
    *// 복잡한 알고리즘, 데이터 처리*
}

*// 파일/네트워크 → Dispatchers.IO*  
launch(Dispatchers.IO) {
    *// API 호출, 파일 읽기, DB 쿼리*
}

*// 화면 업데이트 → Dispatchers.Main*
launch(Dispatchers.Main) {
    *// UI 요소 변경, 사용자 입력 처리*
}

컨텍스트 전환 패턴

*// 전형적인 패턴: Main → IO → Main*
launch(Dispatchers.Main) {
    showLoading()

    val data = withContext(Dispatchers.IO) {
        fetchData()  *// 백그라운드 작업*
    }

    hideLoading()
    showData(data)  *// UI 업데이트*
}

스레드 안전성 체크리스트

  • 단일 코루틴: 데이터 수정 안전
  • 여러 코루틴: Mutex, Atomic 클래스 사용
  • 공유 상태: 동기화 메커니즘 필요

15장 구조화된 동시성 - 코루틴의 질서와 취소


15.1.1 코루틴 스코프 - 코루틴들의 조직

부모-자식 관계 자동 형성

fun main() = runBlocking {  *// 부모 코루틴 (coroutine#1)*
    println("부모 시작")

    launch {  *// 자식 코루틴 (coroutine#2)*
        delay(1000)
        launch {  *// 손자 코루틴 (coroutine#4)*
            delay(250)
            println("손자 완료")
        }
        println("자식 1 완료")
    }

    launch {  *// 자식 코루틴 (coroutine#3)*
        delay(500)
        println("자식 2 완료")
    }

    println("부모가 자식들 시작함")
    *// runBlocking은 모든 자식이 끝날 때까지 기다림!*
}

*// 출력:
// 부모 시작
// 부모가 자식들 시작함
// 자식 2 완료 (500ms 후)
// 자식 1 완료 (1000ms 후)
// 손자 완료 (1250ms 후)*

15.1.1, 15.1.2 coroutineScope vs CoroutineScope

일시 중단 함수인 coroutinescope가 Coroutinescope 생성자 함수보다 더 많이 사용

coroutinescope는 일시 중단 함수의 본문에서 자주 호출되며, CoroutineScope 생성자는 클래
스 프로퍼티로 코루틴 스코프를 저장할 때 주로 사용

1. coroutineScope 함수 - 동시적 작업 분해

concurrent decomposition of work

*// 동시 작업을 위한 임시 스코프*
suspend fun computeSum(): Int {
    println("합계 계산 시작")

    return coroutineScope {  *// 새 스코프 생성, 일시중단 함수*
        val a = async { generateValue() }  *// 비동기 작업 1*
        val b = async { generateValue() }  *// 비동기 작업 2*

        a.await() + b.await()  *// 두 결과 합계*
    } *// 모든 자식이 완료될 때까지 기다림*
}

suspend fun generateValue(): Int {
    delay(500)
    return Random.nextInt(0, 10)
}

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

2. CoroutineScope 생성자 - 생명주기 관리

*// 클래스와 연결된 독립적 스코프*
class ComponentWithScope(
    dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    *// 독립적인 스코프 생성 (일시중단 안함)*
    private val scope = CoroutineScope(dispatcher + SupervisorJob())

    fun start() {
        println("컴포넌트 시작!")

        *// 지속적인 작업*
        scope.launch {
            while (true) {
                delay(500)
                println("작업 중...")
            }
        }

        *// 일회성 작업*
        scope.launch {
            delay(500)
            println("초기화 완료!")
        }
    }

    fun stop() {
        println("컴포넌트 중단!")
        scope.cancel()  *// 스코프와 모든 자식 취소*
    }
}

fun main() {
    val component = ComponentWithScope()
    component.start()
    Thread.sleep(2000)  *// 2초 대기*
    component.stop()
}

15.1.3 GlobalScope의 위험성

문제: 구조화된 동시성 파괴

GlobalScope를 사용하면 구조화된 동시성이 제공하는 모든 이점을 포기

*// GlobalScope 사용 - 위험!*
fun badExample() = runBlocking {
    GlobalScope.launch {  *// 독립적인 전역 스코프*
        delay(1000)
        println("이 메시지는 출력되지 않음")
    }

    println("부모 완료")
    *// runBlocking이 GlobalScope 자식들을 기다리지 않음!*
}

*// 올바른 방법*
fun goodExample() = runBlocking {
    launch {  *// runBlocking의 자식*
        delay(1000)
        println("이 메시지는 출력됨")
    }

    println("부모 완료")
    *// runBlocking이 모든 자식을 기다림*
}

15.2 취소 (Cancellation) - 불필요한 작업 중단

왜 취소가 중요할까?

사용자가 화면을 나갔는데 계속 작업한다면?
네트워크 요청이 실패했는데 다른 요청들이 계속 진행된다면?
→ 리소스 낭비, 메모리 누수

기본 취소 사용법

fun main() = runBlocking {
    val job1 = launch {
        repeat(10) {
            println("작업 1: $it")
            delay(200)
        }
    }

    val job2 = async {
        repeat(10) {
            println("작업 2: $it")
            delay(150)
        }
        "완료!"
    }

    delay(1000)  *// 1초 후*

    job1.cancel()  *// 작업 1 취소*
    job2.cancel()  *// 작업 2 취소*

    println("취소 완료")
}

15.2.2 자동 취소 - 타임아웃

withTimeout 함수는 타임 아웃이 되면 예외 (TimeoutCancellationException)를 발생 시킨다. 타임 아웃을 처리 하려면 withTimeout 호출을 try 블록으로 감싸고, 발생한 TimeoutCancellationException을 잡아내야 한다. 비슷하게 withTimeoutOrNull 함수는 타임아웃이 발생하면 null을 반환한다.

TimeoutCancellationException을 잡지 않으면 호출한 코루틴이 의도와 다르게 취소될 수 있다. 이 문제를 완전히 피하려면 withTimeoutOrNull 함수를 사용하는 편이 좋다.

withTimeoutOrNull 사용

suspend fun longRunningTask(): String {
    delay(3000)  *// 3초 걸리는 작업*
    return "작업 완료!"
}

fun main() = runBlocking {
    *// 1초 타임아웃*
    val result1 = withTimeoutOrNull(1000) {
        longRunningTask()
    }
    println("결과1: $result1")  *// null (타임아웃)*

    *// 5초 타임아웃*  
    val result2 = withTimeoutOrNull(5000) {
        longRunningTask()
    }
    println("결과2: $result2")  *// "작업 완료!"*
}

15.2.3 취소 전파 - 부모 취소하면 자식도 취소

계층적 취소

fun main() = runBlocking {
    val parentJob = launch {
        launch {  *// 자식 1*
            launch {  *// 손자 1*
                repeat(10) {
                    println("손자 작업: $it")
                    delay(200)
                }
            }
        }

        launch {  *// 자식 2*
            repeat(10) {
                println("자식2 작업: $it")
                delay(150)
            }
        }
    }

    delay(1000)
    parentJob.cancel()  *// 부모 취소 → 모든 자식, 손자 자동 취소!*
    println("모든 작업 취소됨")
}

15.2.5 협력적 취소 - 취소에 협조하기

문제: 취소되지 않는 코드

*// 취소되지 않는 CPU 집약적 작업*
suspend fun badCpuWork(): Int {
    var counter = 0
    val startTime = System.currentTimeMillis()

    *// 500ms 동안 계속 계산 (일시중단 지점 없음)*
    while (System.currentTimeMillis() < startTime + 500) {
        counter++
        *// delay나 다른 일시중단 함수 없음!*
    }

    return counter
}

fun main() = runBlocking {
    val job = launch {
        repeat(5) {
            badCpuWork()  *// 취소 안됨!*
            println("작업 $it 완료")
        }
    }

    delay(600)  *// 600ms 후 취소 시도*
    job.cancel()  *// 하지만 취소 안됨...*
}

해결책 1: ensureActive() 사용

*// 취소 가능한 CPU 집약적 작업*
suspend fun goodCpuWork(): Int {
    var counter = 0
    val startTime = System.currentTimeMillis()

    while (System.currentTimeMillis() < startTime + 500) {
        counter++

        if (counter % 1000 == 0) {  *// 주기적으로 확인*
            ensureActive()  *// 취소됐으면 CancellationException 발생*
        }
    }

    return counter
}

해결책 2: yield() 사용 - 다른 코루틴에게 양보

suspend fun cooperativeCpuWork(): Int {
    var counter = 0
    val startTime = System.currentTimeMillis()

    while (System.currentTimeMillis() < startTime + 500) {
        counter++

        if (counter % 1000 == 0) {
            yield()  *// 다른 코루틴에게 실행 기회 제공 + 취소 체크*
        }
    }

    return counter
}

fun main() = runBlocking {
    *// 두 개의 CPU 집약적 작업이 번갈아 실행됨*
    launch {
        repeat(3) {
            cooperativeCpuWork()
            println("작업 A-$it 완료")
        }
    }

    launch {
        repeat(3) {
            cooperativeCpuWork()
            println("작업 B-$it 완료")
        }
    }
}

15.2.8 리소스 정리 - 취소되어도 안전하게

finally로 리소스 정리

class DatabaseConnection : AutoCloseable {
    fun write(data: String) = println("DB에 저장: $data")
    override fun close() = println("DB 연결 종료")
}

fun main() = runBlocking {
    val job = launch {
        val db = DatabaseConnection()

        try {
            delay(500)  *// 취소 지점*
            db.write("중요한 데이터")
        } finally {
            db.close()  *// 취소되어도 반드시 실행됨*
        }
    }

    delay(200)  *// 200ms 후 취소*
    job.cancel()

    *// 출력: "DB 연결 종료"*
}

use 함수로 더 간단하게

fun main() = runBlocking {
    val job = launch {
        DatabaseConnection().use { db ->  *// 자동으로 close() 호출*
            delay(500)
            db.write("중요한 데이터")
        }
    }

    delay(200)
    job.cancel()
}

취소 예외 처리 주의사항

잘못된 예외 처리 - 취소를 막아버림

fun main() = runBlocking {
    withTimeoutOrNull(2000) {
        while (true) {
            try {
                delay(500)
                throw UnsupportedOperationException("에러!")
            } catch (e: Exception) {  *// CancellationException도 잡아버림!*
                println("에러: ${e.message}")
                *// 취소 예외를 삼켜서 무한 루프!*
            }
        }
    }
}

올바른 예외 처리 - 취소 예외는 다시 던지기

fun main() = runBlocking {
    withTimeoutOrNull(2000) {
        while (true) {
            try {
                delay(500)
                throw UnsupportedOperationException("에러!")
            } catch (e: Exception) {
                if (e is CancellationException) throw e  *// 취소 예외는 재던짐*
                println("에러: ${e.message}")
            }
        }
    }
}

실무 패턴들

1. 안드로이드 ViewModel

class UserViewModel : ViewModel() {
    fun loadUserData(userId: String) {
        viewModelScope.launch {  *// ViewModel과 생명주기 연결*
            try {
                val userData = userRepository.getUser(userId)
                _userData.value = userData
            } catch (e: Exception) {
                _error.value = e.message
            }
        }
        *// 화면 나가면 자동으로 취소됨!*
    }
}

2. 서버 요청 처리 (Ktor) - 케이토

routing {
    get("/users/{id}") {  *// 요청 스코프*
        launch {  *// 클라이언트 연결 끊으면 자동 취소*
            val userId = call.parameters["id"]!!
            val userData = processUserData(userId)  *// 오래 걸리는 작업*
            call.respond(userData)
        }
    }
}

3. 백그라운드 작업 관리

class BackgroundService {
    private val serviceScope = CoroutineScope(
        Dispatchers.Default + SupervisorJob()
    )

    fun startPeriodicTask() {
        serviceScope.launch {
            while (true) {
                performMaintenanceTask()
                delay(60000)  *// 1분마다 실행*
            }
        }
    }

    fun shutdown() {
        serviceScope.cancel()  *// 모든 백그라운드 작업 중단*
    }
}

정리

구조화된 동시성의 장점

  1. 자동 대기: 부모가 자식들 완료까지 기다림
  2. 자동 취소: 부모 취소하면 자식들도 자동 취소
  3. 리소스 관리: 메모리 누수, 좀비 코루틴 방지
  4. 예외 전파: 구조화된 오류 처리 (18장에서 자세히)

취소 체크리스트

  • 일시중단 함수: 자동으로 취소 가능
  • CPU 집약적 작업: ensureActive() 또는 yield() 추가
  • 리소스 사용: try-finally 또는 use 사용
  • 예외 처리: CancellationException은 다시 던지기

스코프 선택 가이드

*// 임시 병렬 작업*
coroutineScope { ... }

*// 클래스 생명주기 연결*
CoroutineScope(SupervisorJob() + Dispatchers.Default)

*// 절대 사용 금지*
GlobalScope.launch { ... }  

'language > Kotilin In Action' 카테고리의 다른 글

8주차 (16~18장)  (3) 2025.09.01
6주차 (13장)  (2) 2025.09.01
5주차(11~12장)  (1) 2025.09.01
4주차(9~10장)  (1) 2025.09.01
3주차 (6~8장)  (1) 2025.09.01