Notice
Recent Posts
Recent Comments
Link
250x250
반응형
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

백고등어 개발 블로그

코틀린 강의 9강: 코루틴 기초 본문

코틀린 강의

코틀린 강의 9강: 코루틴 기초

백고등어 2025. 10. 28. 17:40
728x90
반응형

9강: 코루틴 기초

코루틴이란?

코루틴(Coroutine)은 비동기 프로그래밍을 쉽게 작성할 수 있게 해주는 코틀린의 핵심 기능입니다. 경량 스레드라고 불리며, 수천 개의 코루틴을 동시에 실행해도 성능 저하가 거의 없습니다.

코루틴 시작하기

의존성 추가

코루틴을 사용하려면 먼저 의존성을 추가해야 합니다. Gradle의 경우:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}

첫 번째 코루틴

import kotlinx.coroutines.*

fun main() = runBlocking {  // 메인 스레드를 차단하는 코루틴 빌더
    launch {  // 새로운 코루틴 시작
        delay(1000L)  // 1초 대기 (논블로킹)
        println("World!")
    }
    println("Hello,")
}
// 출력:
// Hello,
// (1초 후)
// World!

코루틴 빌더

runBlocking

현재 스레드를 차단하고 코루틴이 완료될 때까지 기다립니다. 주로 main 함수나 테스트에서 사용합니다:

fun main() = runBlocking {
    println("Start")
    delay(1000L)
    println("End")
}

launch

새로운 코루틴을 시작하고 Job을 반환합니다. 결과값을 반환하지 않습니다:

fun main() = runBlocking {
    val job = launch {
        delay(1000L)
        println("코루틴 완료")
    }
    println("메인 코루틴")
    job.join()  // 코루틴이 완료될 때까지 대기
}

async

결과값을 반환하는 코루틴을 시작합니다. Deferred<T>를 반환합니다:

fun main() = runBlocking {
    val deferred = async {
        delay(1000L)
        "결과값"
    }
    println("계산 중...")
    val result = deferred.await()  // 결과를 기다림
    println("결과: $result")
}

일시 중단 함수(Suspend Functions)

suspend 키워드로 표시된 함수는 코루틴 내에서만 호출할 수 있습니다:

suspend fun fetchData(): String {
    delay(2000L)  // 2초 대기
    return "데이터"
}

fun main() = runBlocking {
    println("데이터 가져오는 중...")
    val data = fetchData()
    println("받은 데이터: $data")
}

여러 일시 중단 함수 호출

suspend fun fetchUser(): String {
    delay(1000L)
    return "사용자 정보"
}

suspend fun fetchPosts(): String {
    delay(1500L)
    return "게시글 목록"
}

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    
    // 순차 실행 (약 2.5초 소요)
    val user = fetchUser()
    val posts = fetchPosts()
    
    val endTime = System.currentTimeMillis()
    println("$user, $posts")
    println("소요 시간: ${endTime - startTime}ms")
}

동시 실행

async를 사용한 병렬 실행

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    
    // 동시 실행 (약 1.5초 소요)
    val userDeferred = async { fetchUser() }
    val postsDeferred = async { fetchPosts() }
    
    val user = userDeferred.await()
    val posts = postsDeferred.await()
    
    val endTime = System.currentTimeMillis()
    println("$user, $posts")
    println("소요 시간: ${endTime - startTime}ms")
}

구조화된 동시성(Structured Concurrency)

코루틴은 구조화된 동시성을 따릅니다. 부모 코루틴이 취소되면 자식 코루틴도 모두 취소됩니다:

fun main() = runBlocking {
    val job = launch {
        repeat(5) { i ->
            println("작업 $i")
            delay(500L)
        }
    }
    
    delay(1300L)
    println("취소합니다")
    job.cancel()  // 코루틴 취소
    job.join()    // 취소 완료 대기
    println("완료")
}

코루틴 스코프(Coroutine Scope)

GlobalScope (권장하지 않음)

fun main() = runBlocking {
    GlobalScope.launch {
        delay(1000L)
        println("GlobalScope")
    }
    println("메인")
    delay(2000L)  // GlobalScope가 완료될 때까지 대기
}

CoroutineScope

권장되는 방식은 CoroutineScope를 명시적으로 만들어 사용하는 것입니다:

fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
    
    scope.launch {
        delay(1000L)
        println("코루틴 1")
    }
    
    scope.launch {
        delay(500L)
        println("코루틴 2")
    }
    
    delay(2000L)  // 코루틴들이 완료될 때까지 대기
}

코루틴 컨텍스트와 디스패처

Dispatchers

코루틴이 실행될 스레드를 지정합니다:

fun main() = runBlocking {
    // Dispatchers.Default: CPU 집약적 작업
    launch(Dispatchers.Default) {
        println("Default: ${Thread.currentThread().name}")
    }
    
    // Dispatchers.IO: 네트워크, 파일 I/O 작업
    launch(Dispatchers.IO) {
        println("IO: ${Thread.currentThread().name}")
    }
    
    // Dispatchers.Main: UI 스레드 (Android에서만)
    // launch(Dispatchers.Main) { ... }
    
    // Dispatchers.Unconfined: 제한 없음 (일반적으로 사용하지 않음)
    launch(Dispatchers.Unconfined) {
        println("Unconfined: ${Thread.currentThread().name}")
    }
    
    delay(1000L)
}

withContext를 사용한 컨텍스트 전환

suspend fun fetchDataFromNetwork(): String {
    return withContext(Dispatchers.IO) {
        // I/O 작업
        delay(1000L)
        "네트워크 데이터"
    }
}

fun main() = runBlocking {
    println("메인 스레드: ${Thread.currentThread().name}")
    val data = fetchDataFromNetwork()
    println("데이터: $data")
    println("다시 메인 스레드: ${Thread.currentThread().name}")
}

예외 처리

try-catch

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("작업 $i")
                delay(500L)
            }
        } catch (e: CancellationException) {
            println("취소됨")
            throw e  // CancellationException은 다시 던져야 함
        } finally {
            println("정리 작업")
        }
    }
    
    delay(1300L)
    job.cancel()
    job.join()
}

CoroutineExceptionHandler

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("예외 처리: ${exception.message}")
    }
    
    val job = GlobalScope.launch(handler) {
        throw RuntimeException("오류 발생!")
    }
    
    job.join()
}

타임아웃

withTimeout

fun main() = runBlocking {
    try {
        withTimeout(1300L) {
            repeat(1000) { i ->
                println("작업 $i")
                delay(500L)
            }
        }
    } catch (e: TimeoutCancellationException) {
        println("타임아웃!")
    }
}

withTimeoutOrNull

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("작업 $i")
            delay(500L)
        }
        "완료"
    }
    println("결과: $result")  // 결과: null
}

실전 예제

여러 API 호출 병렬 처리

data class User(val name: String)
data class Posts(val count: Int)
data class Comments(val count: Int)

suspend fun fetchUser(userId: Int): User {
    delay(1000L)
    return User("사용자-$userId")
}

suspend fun fetchPosts(userId: Int): Posts {
    delay(1500L)
    return Posts(10)
}

suspend fun fetchComments(userId: Int): Comments {
    delay(800L)
    return Comments(25)
}

data class UserProfile(val user: User, val posts: Posts, val comments: Comments)

suspend fun fetchUserProfile(userId: Int): UserProfile = coroutineScope {
    val userDeferred = async { fetchUser(userId) }
    val postsDeferred = async { fetchPosts(userId) }
    val commentsDeferred = async { fetchComments(userId) }
    
    UserProfile(
        user = userDeferred.await(),
        posts = postsDeferred.await(),
        comments = commentsDeferred.await()
    )
}

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val profile = fetchUserProfile(1)
    val endTime = System.currentTimeMillis()
    
    println(profile)
    println("소요 시간: ${endTime - startTime}ms")  // 약 1500ms
}

진행 상황 업데이트

suspend fun downloadFile(name: String, progress: (Int) -> Unit) {
    repeat(10) { i ->
        delay(300L)
        progress((i + 1) * 10)
    }
    println("$name 다운로드 완료")
}

fun main() = runBlocking {
    launch {
        downloadFile("파일1.zip") { percent ->
            println("파일1: $percent%")
        }
    }
    
    launch {
        downloadFile("파일2.zip") { percent ->
            println("파일2: $percent%")
        }
    }
    
    delay(5000L)
}

코루틴 사용 시 주의사항

  1. GlobalScope 지양: 생명주기 관리가 어려우므로 명시적인 CoroutineScope 사용을 권장합니다.
  2. 적절한 Dispatcher 선택: CPU 집약적 작업은 Default, I/O 작업은 IO를 사용합니다.
  3. 예외 처리: 코루틴의 예외는 부모로 전파되므로 적절히 처리해야 합니다.
  4. 취소 가능하게 작성: isActive를 확인하거나 ensureActive()를 호출합니다.

마치며

이번 강의에서는 코틀린 코루틴의 기초를 알아보았습니다. 코루틴은 비동기 프로그래밍을 동기 코드처럼 작성할 수 있게 해주는 강력한 도구입니다. launch와 async의 차이, 적절한 Dispatcher 선택, 구조화된 동시성 개념을 잘 이해하면 효율적인 비동기 코드를 작성할 수 있습니다. 다음 강의에서는 코틀린의 고급 기능들을 알아보겠습니다.

728x90
반응형