2장 코루틴 인 액션

코틀린 동시성 프로그래밍 2장을 요약한 내용입니다.

스레드 생성

코틀린은 스레드 생성 과정을 단순화해서 쉽고 간단하게 스레드를 생성할 수 있다. 지금은 단일 스레드만으로도 충분하지만, 이후 과정에서는 CPU 바운드와 I/O 바운드 작업을 모두 효율적으로 수행하기 위해 스레드 풀도 생성할 것이다.

CoroutineDispatcher

코틀린에서는 스레드와 스레드 풀을 쉽게 만들 수 있지만 직접 엑세스하거나 제어하지 않는다는 점을 알아야 한다.

여기에서는 스레드를 하나만 갖는 CoroutineDispatcher를 생성할 것이며, 거기에 추가하는 모든 코루틴은 그 특정 스레드에서 실행된다. 그렇게 하려면 단 하나의 스레드만 갖는 CoroutineDispatcher를 확장한 ThreadPoolDispatcher를 생성한다.

fun main(args: Array<String>) = runBlocking {
    val netDispatcher = newSingleThreadContext(name = "ServiceCall")

    val task = GlobalScope.launch(netDispatcher) {
        printCurrentThread()
    }

    task.join()
}

디스패처에 코루틴 붙이기

디스패처가 만들어졌고 이제 이 디스패처를 사용하는 코루틴을 시작할 수 있다.

async 코루틴 시작

결과 처리를 위한 목적으로 코루틴을 시작했다면 async()를 사용해야 한다. async()는 디퍼드 코루틴 프레임워크에서 제공하는 취소 불가능한 넌 블로킹 퓨처(non-blocking cancellable future)를 의미하여, T는 그 결과의 유형을 나타낸다.

fun main(args: Array<String>) = runBlocking {
    val task = GlobalScope.async {
        doSomething()
    }
		task.join()
		println("Completed")
}

fun doSomething() {
    throw UnsupportedOperationException("Can't do")
}

예외를 통해 애플리케이션 실행이 멈추고 예외 스택이 출력되며 또한 애플리케이션의 종료가 되지 아닐거라고 생각할 수 있다. 그러나 실행해보면 로그에 출력되는 예외 스택은 없으며 애플리케이션도 중단되지 않았고 종료 코드는 성공적으로 실행된 것으로 나타난다.

async() 블록 안에서 발생하는 예외는 그 결과에 첨부되는데, 그 결과를 확인해야 예외를 찾을 수 있다. 이를 위해서 isCancelled와 getCancellationException() 메소드를 함께 사용해 안전하게 예외를 가져올 수 있다.

if (task.isCancelled) {
    val exception = task.getCancellationException()
    println("Error with message: ${exception.cause}")
} else {
    println("Success")
}

예외를 전파하기 위해서 디퍼드에서 await()을 호출할 수 있다.

fun main(args: Array<String>) = runBlocking {
    val task = GlobalScope.async {
        doSomething()
    }

    // This code will have the exception be propagated
    task.await()
		println("Success")
}

그러면 애플리케이션이 비정상적으로 중단된다.

await()를 호출해서 중단되는데 이 경우가 예외를 감싸지 않고 전파하는 감싸지 않은 디퍼드(unwrapping deferred)다.

join()으로 대기한 후 검증하고 어떤 오류를 처리하는 것과 await()를 직접 호출하는 방식의 주요 차이는 join()은 예외를 전파하지 않고 처리하는 반면, await()는 단지 호출하는 것만으로 예외가 전파된다는 점이다.

launch 코루틴 시작

결과를 반환하지 않는 코루틴을 시작하려면 launch()를 사용해야 한다. launch()는 연산이 실패한 경우에만 통보 받기를 원하는 fire-and-forget 시나리오를 위해 설계됐으며, 필요할 때 취소할 수 있는 함수도 함께 제공된다.

fun main(args: Array<String>) = runBlocking {
    val task = GlobalScope.launch {
        doSomething()
    }

    // This code will have the exception be propagated
    task.join()
		println("Success")
}

예상한 대로 예외가 스택에 출력되지만 실행이 중단되지 않았고, 애플리케이션은 main()의 실행을 완료했다는 것을 알 수 있다.

코루틴을 시작할 때 특정 디스패처 사용하기

지금까지는 기본 디스패처를 사용했지만 다음과 같이 특정 코루틴 디스패처를 사용할 수 있다.

fun main(args: Array<String>) = runBlocking {
		val dispatcher = newSingleThreadContext(name = "ServiceCall")
    val task = GlobalScope.launch(dispatcher) {
        doSomething()
    }

    // This code will have the exception be propagated
    task.join()
		println("Success")
}

요약

  • 네트워크 요청은 백그라운드 스레드에서 수행해야 한다. 업데이트되는 뷰를 위한 정보는 UI 스레드로 전달해야 한다.

  • CoroutineDispatcher는 코루틴을 특정 스레드 또는 스레드 그룹에서 실행하도록 할 수 있다.

  • 하나 이상의 코루틴을 launch 또는 async로 스레드에서 실행할 수 있다.

  • launch는 파이어앤포갯(fire-and-forget_와 같은 시나리오에서 사용돼야 하는데, 코루틴이 무언가를 반환할 것을 예상하지 않는 경우를 말한다.

  • 동시 코드를 작성하는 방법에는 여러 가지가 있지만, 명확하고 안전하며 일관성 있게 코틀린의 유연성을 최대한 활용하는 방법을 이해하는 것이 중요하다.

Last updated