코틀린 코루틴의 정석 책 내용 요약 정리
1장 스레드 기반 작업의 한계와 코루틴의 등장 2장 코루틴 개발 환경 설정 3장 CoroutineDispatcher 4장 코루틴 빌더와 Job 5장 async와 Deferred 6장 CoroutineContext 7장 구조화된 동시성 8장 예외 처리 9장 일시 중단 함수 10장 코루틴의 이해 11장 코루틴 심화 12장코루틴 단위 테스트
JVM(Java Virtual Machine)은 자바 애플리케이션을 실행하는 프로세스 내에서 다수의 스레드를 관리하여 동시성을 제공합니다.
단일 스레드 애플리케이션은 한 번에 하나의 작업만 처리할 수 있어, I/O 또는 CPU 집약적 작업 시 응답성이 떨어집니다.
멀티 스레드 프로그래밍을 통해 여러 작업을 동시에 처리함으로써 애플리케이션의 처리량과 응답성을 향상시킬 수 있습니다.
Thread 클래스를 직접 확장하거나 Runnable 인터페이스를 구현하여 사용할 수 있으나, 스레드 관리와 리소스 활용에 한계가 있습니다.
Thread thread = new Thread(() -> System.out.println("Thread running"));
thread.start();
//위 코드는 새 스레드를 생성하여 "Thread running"을 출력합니다.
Executor 프레임워크를 사용하면 스레드풀을 통해 스레드의 생성과 관리를 효율적으로 할 수 있습니다.
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(() -> System.out.println("Running in a thread pool"));
executor.shutdown();
//이 코드는 스레드풀에서 작업을 실행합니다.
멀티 스레드 프로그래밍은 동시성을 제공하지만, 스레드간의 동기화 및 데드락과 같은 복잡성을 관리해야 하는 한계가 있습니다.
기존의 멀티 스레드 프로그래밍은 리소스 관리, 복잡한 동기화 문제 등으로 인해 효율성과 생산성이 떨어질 수 있습니다.
코루틴은 비동기 프로그래밍을 단순화하며, 경량 스레드처럼 동작하되 스레드의 블로킹 없이 비동기 작업을 효율적으로 관리합니다.
suspend fun loadData
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.X.X'
}
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
println(Thread.currentThread().name)
println(coroutineContext[CoroutineName]?.name)
launch {
// 코루틴 내 작업
}
launch(CoroutineName("MyCoroutine")) {
// Named coroutine for debugging
}
CoroutineDispatcher
는 코루틴이 어떤 스레드에서 실행될지를 결정하는 코틀린의 구성 요소입니다.
CoroutineDispatcher
는 코루틴으로 작업을 어떤 스레드로 보낼지 결정하고, 스케줄링하는 원리를 관장합니다.
CoroutineDispatcher
의 주요 역할은 코루틴의 실행을 특정 스레드나 스레드 풀에 할당하는 것으로 이는 코루틴의 병렬성과 성능에 영향을 미칩니다.
- 제한된 디스패처는 고정된 수의 스레드를 사용하는 반면, 무제한 디스패처는 필요에 따라 스레드를 무제한으로 생성할 수 있습니다.
- 일부 작업에 대해 구체적인 스레드 수나 스레드 풀을 제한하기 위해, 코틀린은 특정 스레드 사이즈를 가진 디스패처를 생성할 수 있게 합니다.
val singleThreadContext = newSingleThreadContext("MySingleThread")
//- 이 코드는 코루틴이 실행될 단일 스레드 컨텍스트를 생성합니다.
val multiThreadContext = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
- 이 코드는 고정된 네 개의 스레드를 가진 디스패처를 만들어 멀티 스레드 코루틴을 가능하게 합니다.
CoroutineDispatcher
를 사용해 코루틴을 실행하면, 작업의 실행 위치(스레드)를 세밀하게 제어할 수 있습니다.
launch(singleThreadContext) {
// 이 코루틴은 singleThreadContext에 지정된 단일 스레드에서 실행됩니다.
}
// - 이 코드는 `launch`에 `singleThreadContext`를 제공하여 해당 코루틴을 특정 스레드에서 실행하도록 합니다.
val job = launch(coroutineContext) {
launch {
// 이 자식 코루틴은 부모와 동일한 CoroutineDispatcher를 사용합니다.
}
}
//- 여기서 `coroutineContext`는 부모 코루틴의 컨텍스트를 자식에게 상속시켜 동일한 디스패처에서 실행되도록 합니다.
- Kotlin은
Dispatchers
객체를 통해 미리 정의된 여러 디스패처를 제공합니다, 아래와 같이 특정한 사용 케이스에 적합합니다.
- 입출력 중심의 작업을 위한 디스패처입니다, 파일, 네트워크 작업 등에 최적화되어 있습니다.
- CPU 사용량이 많은 작업을 위한 디스패처로, 기본적으로 사용 가능한 CPU 코어 수에 비례하는 스레드 풀을 사용합니다.
val limitedDispatcher = Dispatchers.Default.limitedParallelism(2)
//- 이 코드는 `Dispatchers.Default`의 병렬성을 제한하여 동시에 실행될 수 있는 스레드의 최대 개수를 설정합니다.
Dispatchers.IO
와Dispatchers.Default
는 두 범주의 작업을 위해 최적화된 공유 스레드풀을 사용하여 자원을 효율적으로 관리합니다.
- 주로 UI 작업을 위해 사용되는 메인 디스패처로, 안드로이드와 같은 일부 환경에서 메인 스레드에서 실행되는 작업에 적합합니다.
join
함수를 이용하면 한 코루틴이 완료되기를 기다린 후 다음 코루틴을 실행하여 작업을 순차적으로 처리할 수 있습니다.
- 코루틴이 동시에 실행될 경우 서로의 결과에 의존하는 작업들이 예상되는 순서대로 완료되지 않을 수 있는 문제가 발생할 수 있습니다.
val job = launch {
// 어떤 처리를 합니다.
}
job.join() // 위에서 시작한 코루틴이 완료될 때까지 기다립니다.
- 위 코드는 `launch`를 통해 시작된 코루틴이 완료될 때까지 현재 스레드 또는 코루틴을 차단하는 `join()` 함수를 호출하여 순차적인 실행을 보장합니다.
joinAll
함수는 여러 코루틴이 모두 완료될 때까지 대기할 수 있도록 하여, 복수의 코루틴을 효과적으로 순차적으로 처리합니다.
joinAll
은 매개변수로 받은 모든 코루틴이 완료될 때까지 현재 스레드 또는 코루틴을 차단합니다.
val job1 = launch { /* ... */ }
val job2 = launch { /* ... */ }
joinAll(job1, job2)
- 위 코드는 `job1`과 `job2` 두 코루틴이 모두 완료될 때까지 대기하도록 하여 두 작업이 모두 끝난 후에 다음 작업을 수행하게 합니다.
CoroutineStart.LAZY
옵션을 사용하면 코루틴을 즉시 시작하지 않고, 필요한 시점까지 실행을 지연시킬 수 있습니다.
- 지연 시작하려는 코루틴을 정의하고
CoroutineStart.LAZY
를 파라미터로 넘겨서 초기화하지 않습니다.
val lazyJob = launch(start = CoroutineStart.LAZY) { /* ... */ }
lazyJob.start() // 이 호출을 통해 명시적으로 코루틴 시작
- 이 코드는 `launch` 시 `CoroutineStart.LAZY`을 설정하여 코루틴을 즉시 시작하지 않습니다. `start()` 메소드로 코루틴을 수동으로 시작합니다.
- 코루틴은
cancel
메소드를 사용해 언제든지 취소할 수 있으며, 이는 코루틴이 더 이상 필요하지 않을 때 사용됩니다.
val job = launch { /* ... */ }
job.cancel() // 코루틴을 취소합니다.
- 이 코드는 실행 중이거나 아직 시작되지 않은 코루틴을 취소하는 `cancel` 메소드를 호출합니다.
val job = launch { /* ... */ }
job.cancelAndJoin() // 코루틴을 취소하고, 완전히 종료될 때까지 기다립니다.
- `cancelAndJoin`은 `cancel`과 `join`을 동시에 수행하여 코루틴을 취소하고 완전히 종료될 때까지 기다리는 효과가 있습니다.
- 코루틴 내부에서는 주기적으로 취소가 발생했는지를 확인하고 적절하게 대응하는 것이 중요합니다.
launch {
try {
delay(Long.MAX_VALUE) // 오랜 시간 동안 대기
} catch (e: CancellationException) {
// 취소 시 예외 처리
}
}
- `delay` 함수는 코루틴이 취소되었을 때 `CancellationException`을 발생시켜 취소 상태를 확인할 수 있습니다.
launch {
while (isActive) { // 코루틴이 활성 상태인지 확인
yield() // 취소를 확인하고 처리합니다.
// ...
}
}
yield
는 현재 코루틴이 다른 코루틴에게 실행을 양보하고 취소되었으면CancellationException
을 발생시켜 처리할 기회를 제공합니다.
launch {
if (!isActive) return@launch // 코루틴이 활성 상태가 아니라면 종료합니다.
// ...
}
- `isActive`는 현재 코루틴의 상태가 활성 상태인지 아닌지를 반환합니다. 이를 이용해 코루틴 내부에서 취소 여부를 확인하고 대응할 수 있습니다.
Job
의 생명 주기는 여러 상태를 가지며, 이를 확인하여 코루틴의 실행 상태를 관리할 수 있습니다.
fun printJobStatus(job: Job) {
println("IsActive: ${job.isActive}, IsCompleted: ${job.isCompleted}, IsCancelled: ${job.isCancelled}")
}
- 이 함수는 `Job` 인스턴스를 매개변수로 받아 현재 활성(active), 완료(completed), 취소(cancelled) 상태를 출력합니다.
- 코루틴이 시작되지 않아 초기 상태에 있는 것을 나타냅니다.
- 코루틴이 실행 중이며 작업이 진행되고 있는 상태입니다.
- 코루틴의 모든 작업이 완료되어 더 이상 진행되지 않는 상태를 의미합니다.
- 코루틴이 취소 요청을 받았으나 아직 완전히 취소 처리가 완료되지 않은 중간 상태입니다.
- 코루틴이 취소 요청을 처리하고 모든 관련 리소스가 정리된 상태입니다.
Job
의 상태를 통해 코루틴의 생명 주기를 정리하고 의도한 동작이 일어나도록 관리할 수 있습니다.
async
는 코루틴을 비동기적으로 실행하여 결과를Deferred
객체로 반환하고, 나중에 그 결과를 받을 수 있게 해주는 함수입니다.
val deferredResult = async { computeSomething() }
//- 이 코드는 `computeSomething()` 함수를 비동기적으로 실행하고, 반환된 값을 나중에 받기 위한 `Deferred` 객체를 만듭니다.
val result = deferredResult.await()
- `await()` 함수를 호출하여 `Deferred` 객체가 가지고 있는 결과값을 비동기적으로 받습니다; 결과가 준비될 때까지 대기합니다.
Deferred
는Job
의 하위 타입으로, 작업이 완료될 때까지의 결과를 담고 있으며, 해당 결과를await()
을 통해 얻을 수 있습니다.
- 여러
async
코루틴이 실행되고 있을 때, 각각의 결과 값을 하나로 모아 수신할 수 있습니다.
val deferredOne = async { computeOne() }
val deferredTwo = async { computeTwo() }
val resultOne = deferredOne.await()
val resultTwo = deferredTwo.await()
//- 위 코드에서 `computeOne()`과 `computeTwo()`는 동시에 비동기적으로 실행되며, 각각의 결과는 `await()`을 통해 수신됩니다.
val results = awaitAll(deferredOne, deferredTwo)
//- `awaitAll` 함수를 사용해 복수의 `Deferred` 객체로부터 모든 결과를 수신할 때까지 대기하고, 결과들을 리스트로 반환받습니다.
val deferredList = listOf(async { computeOne() }, async { computeTwo() })
val results = deferredList.awaitAll()
//- `awaitAll`은 `Deferred` 객체의 리스트에 대해서도 사용할 수 있으며, 모든 객체의 결과를 수신할 때까지 대기합니다.
withContext
는 지정한 컨텍스트(예: 디스패처)에서 코루틴을 실행하고 결과를 직접 반환하는 기능을 제공합니다.
val result = withContext(Dispatchers.Default) { computeSomething() }
//- `withContext(Dispatchers.Default)`를 사용하면 `computeSomething()`이 기본 디스패처에서 실행되고, 실행이 완료되면 결과를 바로 반환합니다.
withContext
는 주어진 컨텍스트로 코루틴을 전환하고, 블록 내의 코드를 실행한 다음 그 결과를 반환하며 원래 컨텍스트로 돌아갑니다.
withContext
를 사용할 때는 코드 블록이 무거울 경우 현재 실행되는 다른 코루틴들이 차단될 수 있으니, 스레드 관리에 주의해야 합니다.
CoroutineContext
는 코루틴의 실행 상태와 스케줄링 등을 제어하는 여러 구성 요소(예:Job
,Dispatcher
)를 포함합니다.
- 코루틴의 행동을 지정하기 위해
CoroutineContext
를 구성하며, 다양한 요소들을 조합하여 커스텀 코루틴 컨텍스트를 만듭니다.
CoroutineContext
는 내부적으로Element
들을 맵처럼 관리하며, 각 요소는 고유한 키를 통해 식별됩니다.
val context = Dispatchers.Default + CoroutineName("MyCoroutine")
//- 이 코드는 기본 스레드 디스패처와 코루틴 이름을 포함하는 새로운 `CoroutineContext`를 생성합니다.
val context = Dispatchers.Default + CoroutineName("MyCoroutine") + CoroutineName("NewCoroutineName")
//- `CoroutineContext`에 같은 종류의 요소를 추가하면, 마지막에 추가한 요소로 이전 요소가 덮어씌워집니다.
val contextA = Dispatchers.Default + Job()
val contextB = CoroutineName("MyCoroutine")
val combinedContext = contextA + contextB
//- `contextA`와 `contextB`를 `+` 연산자를 사용해 합쳐 새로운 `CoroutineContext`를 생성합니다.
val contextWithJob = Dispatchers.Default + Job()
//- `Dispatchers.Default`에 `Job()`을 추가해 코루틴의 취소 및 생명주기 관리를 위한 `Job` 요소를 `CoroutineContext`에 추가합니다.
CoroutineContext
에 저장된 개별 요소에 접근하기 위해get
연산자와 함께 해당 요소의 키를 사용합니다.
- 각
CoroutineContext.Element
에는 고유한 타입의Key
가 있으며, 이 키를 통해Context
내 요소에 접근할 수 있습니다.
val dispatcher = combinedContext[CoroutineDispatcher.Key]
//- `combinedContext`로부터 `CoroutineDispatcher.Key`를 사용해 디스패처 요소에 접근합니다.
CoroutineContext
에서 특정 요소를 제거하기 위해minusKey
연산자와 해당 요소의 키를 사용합니다.
val newContext = combinedContext - CoroutineName.Key
//- 위 코드는 `combinedContext`에서 `CoroutineName` 요소를 제거하여 `newContext`를 생성합니다.
minusKey
를 사용해 요소를 제거할 때 없는 키를 지정한다 하더라도 오류가 발생하지 않으며, 그대로 원래의CoroutineContext
를 반환합니다.
- 코루틴은 기본적으로 부모 코루틴의
CoroutineContext
를 상속받으며, 이를 통해 실행 환경을 공유합니다.
val parentJob = Job()
val scope = CoroutineScope(Dispatchers.Default + parentJob)
scope.launch {
// 이 코루틴은 부모의 CoroutineScope와 CoroutineContext를 상속받습니다.
}
// - 부모 코루틴으로부터 실행 환경을 상속받아 같은 컨텍스트와 스코프 내에서 작업을 수행합니다.
scope.launch(Dispatchers.IO) {
// 이 코루틴은 부모의 CoroutineScope를 상속받았지만, 디스패처는 IO로 덮어씌워 사용합니다.
}
//- 특정 코루틴에서 실행 환경(예: 디스패처)을 변경하여 부모 코루틴의 환경을 덮어쓸 수 있습니다.
- 부모 코루틴에서
launch
등을 사용해 생성된 자식 코루틴의Job
은 별도의 인스턴스이며, 부모와 독립적으로 관리됩니다.
val parentJob = Job()
val childJob = scope.launch(parentJob) {
// ...
}
//- 부모 코루틴의 `Job`을 자식 코루틴에게 직접 전달하여 구조화된 동시성을 통해 작업 관리를 수행합니다.
- 코루틴의 구조화는 작업의 제어를 용이하게 하며, 취소 및 완료 등의 동작을 코루틴 트리에 전파합니다.
- 부모 코루틴이 취소되면 그 자식 코루틴들 역시 취소되어 취소가 모든 하위 코루틴에 전파됩니다.
- 부모 코루틴은 자신의 모든 자식 코루틴들이 완료될 때까지 자동으로 기다림으로써 작업의 단계적 완료를 보장합니다.
CoroutineScope
를 생성하여 코루틴의 생명주기를 관리하고, 스코프 내 코루틴들 사이의 종속성을 설정합니다.
val scope = CoroutineScope(Dispatchers.Default)
//- `CoroutineScope` 객체를 생성하여 코루틴을 그룹화하고, 제어 및 관리의 기점으로 사용합니다.
- 생성된
CoroutineScope
는 코루틴에 컨텍스트와 생명주기를 전달하여 실행 환경을 제공합니다.
CoroutineScope
내에서 생성된 코루틴은 해당 스코프에 종속되어 스코프의 생명주기에 맞춰 실행됩니다.
scope.cancel()
//- `scope.cancel()`을 호출하여 `CoroutineScope` 내 모든 코루틴을 취소하고 자원을 해제할 수 있습니다.
if (scope.isActive) {
// CoroutineScope 내 코루틴들이 아직 활성화 상태인지 확인
}
//- `CoroutineScope`의 `isActive` 속성을 통해 스코프의 활성 상태를 확인할 수 있습니다.
Job
은 코루틴의 생명주기를 대표하며, 구조화된 동시성을 통해 코루틴 구조와 종속성을 관리합니다.
runBlocking {
// 이 블록 내에서 생성된 코루틴들은 runBlocking의 Job을 상속받습니다.
}
//- `runBlocking` 내에서 시작된 코루틴은 `runBlocking`의 `Job`을 상속받아 계층적 구조를 형성합니다.
- 자식 코루틴을 다른 스코프로 이동하거나 고립시켜 부모-자식 관계를 끊을 수 있으며, 이 경우 부모 코루틴의 취소가 자식 코루틴에 영향을 미치지 않습니다.
val independentJob = Job()
scope.launch(independentJob) {
// 이 코루틴은 스코프가 취소되어도 취소되지 않습니다.
}
//- 별도의 `Job` 객체를 사용하여 `CoroutineScope`의 취소로부터 독립된 코루틴을 실행할 수 있습니다.
- 코루틴의
Job
파라미터를 명시적으로 설정함으로써 부모-자식 관계를 설정하고, 구조화된 작업 흐름을 만들 수 있습니다.
- 코루틴이
Job
과 함께 시작되었을 때, 이 작업의 완료는 자동으로 발생하지 않으며 개발자가 관리해야 합니다.
코루틴에서 발생한 예외는 기본적으로 코루틴 계층을 따라 상위로 전파되며, 이를 통해 오류를 처리할 수 있습니다.
코루틴 내부에서 발생한 예외는 부모 코루틴까지 전파되고, 최종적으로 이를 처리하지 않으면 애플리케이션을 종료시킬 수 있습니다.
launch {
throw Exception("This exception will be propagated to the parent coroutine")
}
//- 이 코드에서 `launch` 내부에서 발생하는 예외는 외부로 전파되어 코루틴의 부모에게 도달합니다.
특정 구성 요소를 사용해 코루틴에서 발생하는 예외의 전파를 제한하고, 더 상위로의 전파를 방지할 수 있습니다.
val job = Job()
launch(job) { // 예외 처리를 담당하는 코드 } job.invokeOnCompletion { exception -> if (exception is MyException) handleException(exception) }
//- `Job` 객체와 `invokeOnCompletion`을 사용하여 특정 예외를 처리하고 전파를 제한할 수 있습니다.
val job = SupervisorJob()
launch(job) { // 자식 코루틴에서 발생한 예외는 다른 자식 코루틴에 영향을 미치지 않습니다. }
//- `SupervisorJob`을 사용하면 자식 코루틴 중 하나에서 발생한 예외가 다른 자식들에게 영향을 주지 않습니다.
supervisorScope {
launch {
// 이 블록 내부에서 발생한 예외는 해당 스코프 내 다른 코루틴에 영향을 주지 않습니다.
}
}
//- `supervisorScope`는 내부 코루틴의 예외가 동일 스코프의 다른 코루틴에게 영향을 주지 않도록 합니다.
코루틴에서 발생하는 예외를 전역적으로 잡아 처리하기 위해 CoroutineExceptionHandler
가 사용됩니다.
val handler = CoroutineExceptionHandler { _, exception -> handleException(exception) }
//- `CoroutineExceptionHandler`를 생성해 코루틴 내부에서 처리되지 않은 예외를 잡아 처리할 수 있습니다.
launch(handler) {
// 예외가 여기서 발생하면 CoroutineExceptionHandler가 처리합니다.
}
//- `CoroutineExceptionHandler`를 코루틴 컨텍스트에 추가하여 예외를 처리합니다.
CoroutineExceptionHandler
는 코루틴 내에서 처리되지 않은 예외에 대해서만 동작합니다.
코루틴 컨텍스트에 CoroutineExceptionHandler
를 명시적으로 추가하여 예외 처리를 위임합니다.
CoroutineExceptionHandler
는 예외를 잡기는 하지만 자동으로는 예외의 전파를 중단시키지 않습니다.
코루틴 내부에서 발생하는 예외는 전통적인 try-catch 문을 사용하여 직접 잡고 처리할 수 있습니다.
launch {
try {
// 예외가 발생할 수 있는 코드
} catch (e: Exception) {
handleException(e)
}
}
//- 이 코드는 코루틴 내에서 발생할 수 있는 예외를 try-catch문을 이용해 직접 잡고 처리합니다.
코루틴 빌더 함수 외부에 있는 try-catch문은 해당 코루틴 내부에서 발생한 예외를 잡지 못합니다.
async
빌더로 작성한 코루틴은 예외를 Deferred
객체를 통해 전파하고, 이는 await
호출 시점에 노출됩니다.
val deferred = async {
throw Exception("Error in async")
}
try {
deferred.await()
} catch (e: Exception) {
handleException(e)
}
//- `async` 코루틴의 예외는 `await` 호출 시 노출되고, 이를 통해 예외를 처리할 수 있습니다.
async
로 생성된 Deferred
객체에서 await
은 코루틴 내부에서 발생한 예외를 호출자에게 전파합니다.
특정 예외는 코루틴 실행을 제어하기 위한 목적으로 사용될 때 전파되지 않습니다.
CancellationException
은 코루틴의 정상적인 취소를 나타내며, 이는 일반적으로 코루틴의 부모에게 전파되지 않습니다.
JobCancellationException
은 코루틴의 취소를 나타내는 예외로, 이 역시 취소를 나타내는 데 사용됩니다.
withTimeout(1000L) {
// 시간이 초과되면 TimeoutCancellationException이 발생해 코루틴이 취소됩니다.
}
//- `withTimeout`은 정해진 시간 내에 코루틴이 완료되지 않으면 `TimeoutCancellationException`을 발생시켜 코루틴을 취소합니다.
핵심내용: 일시 중단 함수는 실행을 일시 중단할 수 있고, 나중에 다시 재개할 수 있는 특별한 종류의 함수입니다. 코드 예시:
suspend fun fetchData(): Data {
// 데이터를 비동기적으로 가져오는 로직
}
//설명: suspend 키워드를 사용하여 정의된 fetchData 함수는 비동기 작업을 수행하는 동안 실행을 일시 중단하고, 작업 완료 후 재개됩니다.
핵심내용: 일시 중단 함수 자체는 코루틴이 아니지만, 코루틴 내에서 실행될 수 있습니다. 코드 예시:
GlobalScope.launch {
val data = fetchData() // 일시 중단 함수 호출
}
//설명: fetchData 일시 중단 함수는 GlobalScope.launch 코루틴 내에서 호출됩니다. 이는 일시 중단 함수가 코루틴 내부에서 실행될 수 있음을 보여줍니다.
핵심내용: 일시 중단 함수는 코루틴 빌더를 사용하여 별도의 코루틴으로 실행될 수 있습니다. 코드 예시:
launch {
val data = async { fetchData() }.await()
}
//설명: async 빌더는 fetchData 함수를 별도의 코루틴으로 실행하며, await를 통해 그 결과를 기다립니다.
핵심내용: 일시 중단 함수는 다른 일시 중단 함수나 코루틴 내부에서만 호출될 수 있습니다. 코드 예시:
suspend fun process() {
val data = fetchData() // 다른 일시 중단 함수 내에서 호출
}
//설명: process 일시 중단 함수 내에서 fetchData 일시 중단 함수를 호출하는 예시입니다.
핵심내용: 일시 중단 함수 내에서도 코루틴을 실행하여 비동기 작업을 수행할 수 있습니다. 코드 예시:
suspend fun loadData() {
coroutineScope {
val data = async { fetchData() }
// 데이터 처리 로직
}
}
//설명: coroutineScope 블록 내에서 async를 사용하여 fetchData 함수를 비동기적으로 실행하는 방법을 보여줍니다.
- 서브루틴은 특정 코드 블록을 실행한 다음 완료될 때까지 흐름을 차단하는 함수의 한 형태이며, 코루틴은 코드 실행 중 중단과 재개가 가능한 일반화된 서브루틴입니다.
- 일반적으로 루틴은 프로그램의 실행 흐름을 구성하는 코드 블록이며, 서브루틴은 자신이 호출된 지점으로부터 실행을 시작하고 반환하는 루틴입니다.
- 서브루틴은 단방향의 실행 흐름을 가지며 호출 시점부터 종료까지 블록킹 방식으로 실행되지만, 코루틴은 양방향의 실행 흐름을 가질 수 있으며 중단과 재개가 가능합니다.
- 코루틴에서 스레드를 양보한다는 것은 현재 코루틴의 실행을 일시 중지하고 다른 코루틴이나 태스크가 실행될 수 있도록 스케줄러에게 기회를 준다는 의미입니다.
launch {
delay(1000) // 현재 코루틴을 1초간 중단하고, 그 동안 다른 코루틴이 작업을 할 수 있게 합니다.
}
//- `delay` 함수는 코루틴을 일시 중단하고 현재 스레드를 다른 대기중인 코루틴에게 양보합니다.
launch {
val job = launch { /*...*/ }
job.join() // job 코루틴이 완료될 때까지 현재 코루틴을 중단합니다.
}
//- `join`은 다른 코루틴(`job`)이 완료될 때까지 현재 코루틴을 중단시키며, `await`은 `async`에서 시작된 코루틴의 결과값을 기다립니다.
launch {
yield() // 다른 코루틴에게 실행을 양보하고 나중에 이어서 실행됩니다.
}
//- `yield`는 현재 코루틴이 실행 중인 스레드를 양보하고, 코루틴 디스패처에 의해 나중에 재개될 수 있도록 스케줄을 조정합니다.
- 코루틴의 실행 스레드는 고정되어 있지 않으며, 컨텍스트 또는 디스패처에 따라 실행 스레드가 변경될 수 있습니다.
withContext(Dispatchers.IO) {
// 이 블록은 IO 최적화된 스레드에서 실행됩니다.
}
//- 코루틴 컨텍스트를 변경하여 `withContext`를 사용하면 코루틴이 실행되는 스레드를 변경할 수 있습니다.
- 일반적으로 코루틴 안에서 스레드를 명시적으로 양보하지 않는 한, 코루틴은 시작된 스레드에서 계속 실행됩니다.
- 공유 상태를 사용할 때 코루틴 사이에서 발생할 수 있는 데이터 충돌과 메모리 가시성 문제는 동기화 기법을 사용해 해결할 수 있습니다.
- 여러 코루틴이 동일한 가변 변수에 동시에 접근하면 예상치 못한 결과를 초래할 수 있는 경쟁 상태(race condition)가 발생할 수 있습니다.
- JVM은 하드웨어 메모리 구조와 자신의 메모리 모델을 매핑하여, 코드에서 작성한 메모리 연산이 하드웨어에서 어떻게 수행될지 결정합니다.
- 멀티스레드 환경에서 메모리 가시성 문제는
volatile
키워드 사용, synchronized 블록, 또는 동시성 라이브러리를 적용해 해결할 수 있습니다.
- 경쟁 상태는 코루틴의 동기화 기법(예: Mutex, 채널)을 사용하여, 한 시점에 하나의 코루틴만이 자원에 접근하도록 하여 해결할 수 있습니다.
CoroutineStart
는 코루틴이 어떻게 시작될지 조절하는 옵션으로, 코루틴의 실행 시점과 방식을 제어합니다.
- 코루틴을 기본적으로 즉시 스케줄링하여 실행하지만, 코루틴 디스패처가 멈춤 표시되면 실행이 지연될 수 있습니다.
- 코루틴을 즉시 실행하지만, 시작 후 취소가 불가능한 상태로 만듭니다, 즉 취소가 지연됩니다.
- 코루틴을 현재 스레드에서 즉시 실행하고, 첫 번째 일시 중단 지점까지 디스패치하지 않고 진행합니다.
Dispatcher.Unconfined
는 코루틴을 호출한 스레드에서 시작하지만, 첫 일시 중단 포인트 이후에는 재개된 호출 스레드에서 실행됩니다.
- 무제한 디스패처는 코루틴이 시작되는 스레드를 제한하지 않고, 코루틴이 어떤 스레드에서 재개될지를 운영체제의 스레드 스케줄러에 의해 결정하게 합니다.
Dispatcher.Unconfined
는 특정 스레드 상황에 구애받지 않으며, 코루틴이 실행될 때 스레드를 전환할 필요가 없을 때 유용합니다.
- 코루틴은 Continuation-Passing Style을 사용하여 코루틴의 중단과 재개를 가능하게 하며, 일시 중단 함수를 통해 비동기 작업을 효과적으로 처리합니다.
- CPS(Continuation-Passing Style)는 프로그램의 제어 흐름을 명시적인 Continuation으로 전달하는 프로그래밍 스타일입니다.
- 코루틴의 일시 중단과 재개는 코루틴이 현재 지점에서 중단될 수 있는 흐름과 이를 다시 재개할 방법을
Continuation
으로 관리합니다.
suspend fun doSomething(): Int {
return withContext(Dispatchers.IO) {
// 길이 있는 작업 수행
42 // 결과 반환
}
}
launch {
val result = doSomething() // doSomething이 완료된 후 결과를 받아와 코루틴이 재개됩니다.
}
//- 위 예제에서 `doSomething()` 함수는 `withContext`를 사용하여 IO 디스패처에서 긴 작업을 수행하며, 완료되면 결과 값을 반환하고 코루틴을 재개합니다. `launch` 코루틴 빌더내에서 `doSomething()`의 결과를 기다리므로, 해당 함수가 완료될 때까지 현재 코루틴은 재개되지 않습니다.
- 단위 테스트는 소프트웨어의 가장 작은 실행 단위인 함수나 메서드의 정확성을 검증하는 테스트입니다.
- 단위 테스트는 개별 모듈이 나타내야 하는 기능이 올바르게 작동하는지 확인하는 자동화된 스크립트입니다.
- 테스트에 필요한 라이브러리를 프로젝트에 추가하고, 테스트할 클래스와 메서드를 정의합니다.
@Test
fun `returns true when input is valid`() {
assertTrue(MyClass().validate("valid input"))
}
//- 위 테스트 코드는 `MyClass`의 `validate` 함수가 "valid input"에 대해 `true`를 반환하는지 검사합니다.
@BeforeEach
어노테이션이 붙은 메서드는 각 테스트 실행 전에 호출되어 공통의 초기화 작업을 수행합니다.
- 의존성을 가진 객체를 대신할 수 있는 가짜 객체(테스트 더블)를 사용하여 격리된 환경에서 단위 테스트를 수행합니다.
- 코틀린 코루틴을 사용한 코드의 단위 테스트는 runBlocking을 사용하여 동기적으로 테스트할 수 있습니다.
@Test
fun testCoroutine() = runBlocking {
val result = mySuspendFunction()
assertEquals(expectedValue, result)
}
//- 이 코드는 `mySuspendFunction` 함수가 예상된 값(`expectedValue`)을 반환하는지 테스트합니다.
runBlocking
을 사용할 때 테스트는 함수 내 코드의 완료를 기다릴 수 있지만, 실제 애플리케이션 환경과 달리 메인 스레드를 차단합니다.
- 코루틴을 사용하는 코드를 테스트하기 위해 코틀린은 전용 테스트 라이브러리를 제공합니다.
- 프로젝트의
build.gradle
파일에 코루틴 테스트 라이브러리를 테스트 의존성으로 추가하여 사용할 수 있습니다.
TestCoroutineScheduler
를 사용하면 코루틴의 지연 실행을 개발자가 제어할 수 있으며 테스트 실행 속도를 향상시킬 수 있습니다.
StandardTestDispatcher
는 코루틴의 실행을TestCoroutineScheduler
와 연결하여 가상 시간에 따라 코루틴을 스케줄링합니다.
TestScope
는TestCoroutineDispatcher
를 포함하며 코루틴 테스트를 위한 가상 시간을 제공합니다.
@Test
fun testWithRunTest() = runTest {
val result = mySuspendFunction()
assertEquals(expectedValue, result)
}
//- `runTest`는 `runBlocking`의 향상된 버전으로 가상 시간을 사용하여 테스트를 쉽게 실행하고 관리할 수 있습니다.
- 코루틴을 구현한 코드를 단위 테스트 작성을 통해 검증하면서 코루틴의 라이프사이클과 오류 처리를 이해할 수 있습니다.
- 테스트 대상이 될 코루틴 기반의 클래스와 함수를 정의합니다.
FollowerSearcher
같은 클래스는 코루틴에 기반한 네트워크 호출 등을 모방하여 테스트 코드를 작성합니다.
- 심화된 코루틴 테스트는 코루틴의 이해도를 높이고, 복잡한 비동기 작업에 대한 테스팅 전략을 개발할 수 있게 합니다.
- 객체가 내부적으로 코루틴을 실행할 때, 테스트 상에서 해당 코루틴이 완료될 때까지 기다릴 수 있어야 합니다.
backgroundScope
라는 별도의 스코프를 사용해 비동기 작업을 시작하고, 테스트에서 이를 관리합니다.