프로그래밍 언어/Kotlin

[Kotlin] 코루틴 (Coroutines) 활용법

ioh'sDeveloper 2025. 1. 5. 17:30

Kotlin Coroutines 활용법

코틀린(Kotlin)의 코루틴(Coroutines)은 비동기 프로그래밍을 보다 간결하고 효율적으로 구현할 수 있는 강력한 기능이다. 실무에서 코루틴을 활용하면 I/O 작업, 네트워크 통신, 대용량 데이터 처리 등 다양한 비동기 작업을 효율적으로 처리할 수 있다. 본 글에서는 코루틴의 기본 개념부터 실무에서의 활용 사례까지 다루며, 고급 기법을 포함한 실질적인 사용법을 공유하고자 한다.

1. 코루틴의 기본 개념

코루틴은 경량 스레드(lightweight thread)라고도 불리며, 스레드와 달리 컨텍스트 스위칭의 부하가 적고, 더 많은 비동기 작업을 효율적으로 처리할 수 있다. 코루틴은 다음과 같은 핵심 개념을 바탕으로 동작한다.

  • Suspend Function: 일시 중단(suspend) 기능을 제공하여 함수 실행을 중단하고, 다시 재개할 수 있다.
  • Coroutine Scope: 코루틴이 실행되는 컨텍스트를 정의하며, 코루틴의 생명주기를 관리한다.
  • Dispatcher: 코루틴이 어느 스레드에서 실행될지를 결정한다. 주로 Dispatchers.IO, Dispatchers.Main, Dispatchers.Default 등이 사용된다.
fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}

위 코드에서 runBlocking은 현재 스레드를 블록하며, launch를 통해 코루틴을 시작한다. delay 함수는 비동기적으로 일시 중단된 후 재개된다.


2. Coroutine Builders

코루틴 빌더는 코루틴을 생성하고 시작하기 위한 함수이다. 대표적인 코루틴 빌더는 다음과 같다.

  • launch: 결과를 반환하지 않는 코루틴을 생성한다. 주로 fire-and-forget 작업에 사용된다.
  • async: 결과를 반환하는 코루틴을 생성한다. Deferred 객체를 통해 결과를 비동기적으로 받을 수 있다.
  • runBlocking: 현재 스레드를 차단하며 코루틴을 실행한다. 주로 테스트 코드나 메인 함수에서 사용된다.

예제: launch와 async의 차이점

fun main() = runBlocking {
    val job = launch {
        println("Launch: ${Thread.currentThread().name}")
    }

    val deferred = async {
        println("Async: ${Thread.currentThread().name}")
        42
    }

    job.join() // launch 완료 대기
    println("Async Result: ${deferred.await()}") // async 결과 대기
}

3. Coroutine Context와 Dispatchers

코루틴 컨텍스트는 코루틴이 실행되는 환경을 정의하는 요소이다. 이를 통해 실행되는 스레드나 코루틴의 Job을 관리할 수 있다.

  • Dispatchers.Main: UI 스레드에서 코루틴을 실행한다. 안드로이드 개발에서 주로 사용된다.
  • Dispatchers.IO: I/O 작업에 최적화된 스레드 풀에서 코루틴을 실행한다.
  • Dispatchers.Default: CPU 집약적인 작업에 적합하다.
  • Dispatchers.Unconfined: 호출된 스레드에서 코루틴을 시작하며, 일시 중단 후 재개 시 다른 스레드에서 실행될 수 있다.
fun main() = runBlocking {
    launch(Dispatchers.IO) {
        println("Running on IO Dispatcher: ${Thread.currentThread().name}")
    }
}

4. 실무 활용 사례

4.1. 네트워크 요청 처리

코루틴은 비동기 네트워크 요청을 간결하게 처리할 수 있다. Retrofit과 같은 라이브러리와 함께 사용하면 더욱 효율적이다.

interface ApiService {
    @GET("/users")
    suspend fun getUsers(): List<User>
}

fun fetchUsers(apiService: ApiService) = CoroutineScope(Dispatchers.IO).launch {
    try {
        val users = apiService.getUsers()
        withContext(Dispatchers.Main) {
            // UI 업데이트
        }
    } catch (e: Exception) {
        // 에러 처리
    }
}

4.2. 데이터베이스 작업 최적화

Room과 같은 ORM 라이브러리와 함께 코루틴을 사용하면 비동기 데이터베이스 작업을 간단하게 구현할 수 있다.

@Dao
interface UserDao {
    @Insert
    suspend fun insertUser(user: User)

    @Query("SELECT * FROM users")
    suspend fun getUsers(): List<User>
}

4.3. 병렬 처리

코루틴을 사용하면 여러 비동기 작업을 병렬로 실행하고 결과를 효율적으로 결합할 수 있다.

suspend fun fetchMultipleData(): List<Data> = coroutineScope {
    val data1 = async { fetchData1() }
    val data2 = async { fetchData2() }
    listOf(data1.await(), data2.await())
}

5. 에러 처리와 예외 관리

코루틴에서는 예외 처리를 위해 try-catch 블록과 CoroutineExceptionHandler를 사용할 수 있다.

val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    println("Exception handled: ${exception.localizedMessage}")
}

fun main() = runBlocking {
    launch(exceptionHandler) {
        throw Exception("Something went wrong")
    }
}

6. 테스트 코드 작성 시 코루틴 활용

코루틴을 사용한 코드를 테스트할 때는 runBlockingTest와 TestCoroutineDispatcher를 활용할 수 있다.

@ExperimentalCoroutinesApi
class MyViewModelTest {
    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
    }

    @Test
    fun testMyFunction() = runBlockingTest {
        // 테스트 코드 작성
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

결론

코루틴은 코틀린 개발자에게 비동기 프로그래밍을 위한 강력한 도구를 제공한다. 기본 개념을 잘 이해하고 실무에 적절히 적용하면 코드의 가독성과 성능을 동시에 향상시킬 수 있다. 특히 네트워크 요청, 데이터베이스 작업, 병렬 처리와 같은 작업에서 코루틴의 장점을 극대화할 수 있다. 앞으로의 개발 트렌드에서 코루틴의 중요성은 더욱 커질 것이므로, 이를 적극적으로 활용하는 것이 필수적이다.