
코루틴을 실행하려면 코루틴 빌더(Coroutine Builder)를 사용해야 합니다. 각각의 빌더는 서로 다른 목적과 특성을 가지고 있어, 상황에 맞게 선택해야 합니다.
launch는 가장 일반적으로 사용되는 코루틴 빌더입니다. 결과를 반환하지 않고, 백그라운드에서 작업을 수행할 때 사용합니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
// Job을 반환하는 launch
val job = launch {
delay(1000)
println("World!")
}
println("Hello,")
job.join() // 코루틴 완료까지 대기
}
launch는 block 파라미터의 리시버 타입이 CoroutineScope이므로 코루틴 스코프 내에서만 호출할 수 있습니다.
launch의 특징:
Job 객체를 반환async는 결과값을 반환하는 코루틴을 만들 때 사용합니다. 병렬 처리가 필요한 상황에서 특히 유용합니다.
fun main() = runBlocking {
// Deferred<String>을 반환하는 async
val deferred1 = async {
delay(1000)
"첫 번째 결과"
}
val deferred2 = async {
delay(1500)
"두 번째 결과"
}
// 두 작업이 병렬로 실행되어 총 1.5초만 소요
println("${deferred1.await()} + ${deferred2.await()}")
}
async 또한 block 파라미터의 리시버 타입이 CoroutineScope이므로 코루틴 스코프 내에서만 호출할 수 있습니다.
async의 특징:
Deferred<T> 객체를 반환 (Job의 하위 타입)await()로 결과값을 받아올 수 있음runBlocking은 코루틴 세계와 일반 함수 세계를 연결하는 역할을 합니다. 현재 스레드를 블록하면서 코루틴을 실행합니다. 다른 코루틴 빌더와 달리 CoroutineScope의 확장 함수가 아니므로 자식 코루틴이 될 수 없고, 루트 코루틴으로만 사용될 수 있습니다. 이는 runBlocking이 다른 코루틴 빌더와 근본적으로 다른 쓰임새를 가지는 이유입니다.
과거에는 테스트 코드용 빌더로 많이 사용되었지만, 현재는 가상 시간으로 실행시키는 runTest가 더 많이 사용되고 있어서 현재는 거의 사용되지 않습니다.
fun main() {
// 메인 스레드를 블록하면서 코루틴 실행
runBlocking {
delay(1000)
println("runBlocking 완료!")
}
println("이 메시지는 1초 후에 출력됩니다")
}
runBlocking 사용 시기:
코루틴 컨텍스트는 코루틴이 실행되는 환경을 정의합니다. 여러 요소들의 조합으로 구성됩니다.
// 컨텍스트 구성 요소들
val context = Dispatchers.IO +
CoroutineName("MyCoroutine") +
CoroutineExceptionHandler { _, exception ->
println("예외 발생: $exception")
}
launch(context) {
// 이 코루틴은 IO 스레드에서 실행되고
// "MyCoroutine"이라는 이름을 가지며
// 지정된 예외 핸들러를 사용함
}
주요 컨텍스트 요소:
스코프는 코루틴의 생명주기를 관리하는 영역입니다. 모든 코루틴은 스코프 내에서 실행되어야 합니다.
class MyRepository : CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext = Dispatchers.IO + job
fun loadData() {
launch {
// 이 코루틴은 Repository의 스코프에서 실행
val data = fetchDataFromNetwork()
saveToDatabase(data)
}
}
fun cleanup() {
job.cancel() // 모든 자식 코루틴 취소
}
}
안드로이드에서의 스코프 활용:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// lifecycleScope: 액티비티 생명주기와 연동
lifecycleScope.launch {
// 액티비티가 destroy되면 자동으로 취소
}
// viewModelScope: ViewModel 생명주기와 연동
viewModelScope.launch {
// ViewModel이 clear되면 자동으로 취소
}
}
}
Job은 코루틴이 상속하지 않은 유일한 코루틴 컨텍스트입니다. 모든 코루틴은 자신만의 Job을 생성하고, 인자 또는 부모 코루틴으로부터 온 Job은 새로운 Job의 부모로 사용됩니다.
부모 Job은 자식 Job 모두를 참조할 수 있으며 자식 또한 부모를 참조할 수 있기 때문에 코루틴 스코프 내에서 취소와 예외처리 구현이 가능합니다.
Job을 변수에 할당하는 이유는 코루틴을 제어하기 위해서입니다. launch는 변수에 할당되든 안 되든 항상 즉시 실행되며, 변수 할당은 단순히 Job 참조를 보관하여 나중에 제어할 수 있게 하는 것입니다.
fun demonstrateJobControl() = runBlocking {
// ❌ 변수에 할당하지 않으면 제어 불가능
launch {
repeat(10) {
println("제어 불가능한 코루틴: $it")
delay(500)
}
}
// 이 코루틴은 취소하거나 완료를 기다릴 방법이 없음!
// ✅ 변수에 할당하면 제어 가능
val controlledJob = launch {
repeat(10) {
println("제어 가능한 코루틴: $it")
delay(500)
}
}
delay(2000)
controlledJob.cancel("사용자가 중단 요청")
controlledJob.join() // 취소 완료까지 대기
}
Job 제어의 주요 목적들:
cancel(), join(), cancelAndJoin()isActive, isCompleted, isCancelled
Job은 코루틴의 생명주기를 나타내는 객체로, 다음과 같은 6가지 상태를 가집니다:
fun demonstrateJobStates() = runBlocking {
val job = launch {
delay(1000)
println("작업 완료")
}
println("Active: ${job.isActive}") // true (Active 상태)
println("Completed: ${job.isCompleted}") // false
job.join() // Completed나 Cancelled 상태에 도달할 때까지 대기
println("Completed: ${job.isCompleted}") // true (Completed 상태)
}
join() 메서드는 지정한 Job이 Completed나 Cancelled와 같은 마지막 상태에 도달할 때까지 기다리는 중단 함수입니다. 이는 코루틴을 다시 실행하는 것이 아니라, 이미 실행 중인 코루틴의 완료를 기다리는 것입니다.
fun demonstrateJoinBehavior() = runBlocking {
val job = launch {
println("1. 코루틴 시작!")
delay(2000)
println("2. 코루틴 완료!")
}
println("3. launch 호출 직후 (코루틴은 이미 실행 중)")
job.join() // 여기서 완료까지 기다림 (재실행 아님)
println("4. join() 완료 - 코루틴이 끝났음")
}
코루틴에서의 취소는 코루틴 스스로 취소 신호를 확인하고 종료해야 합니다. 코루틴의 취소는 진행중에서 강제로 종료될 수 없어 중단점에서 취소가 진행됩니다.
cancel() 메서드는 다음과 같은 특징을 가집니다:
cancel(cause) 형태로 취소 원인 지정 가능fun demonstrateCancellation() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("작업 중... $i")
delay(500) // 취소 가능한 지점 - CancellationException 발생 지점
}
} catch (e: CancellationException) {
println("작업이 취소되었습니다: ${e.message}")
// 정리 작업 수행
} finally {
println("정리 작업 완료")
}
}
delay(1300)
job.cancel("사용자가 취소함") // 취소 원인과 함께 취소
job.join() // 취소 완료까지 대기
// 또는 job.cancelAndJoin() 사용 가능
}
코루틴은 CPU 집약적인 작업은 delay() 같은 중단점이 없어 취소가 어려울 수 있습니다.
fun handleNonCancellableWork() = runBlocking {
val job = launch {
var nextPrintTime = System.currentTimeMillis()
var i = 0
// isActive로 취소 상태 확인
while (isActive && i < 5) {
if (System.currentTimeMillis() >= nextPrintTime) {
println("작업 중... ${i++}")
nextPrintTime += 500
}
}
}
delay(1300)
job.cancelAndJoin()
}
코루틴에서 예외가 발생하면 부모-자식 관계에 따라 전파됩니다.
fun demonstrateExceptionPropagation() = runBlocking {
try {
launch {
delay(100)
throw Exception("자식 코루틴 예외")
}
} catch (e: Exception) {
// 이 블록은 실행되지 않음!
println("예외 잡힘: $e")
}
delay(200)
println("부모 코루틴 완료") // 이것도 실행되지 않음
}
fun useExceptionHandler() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("예외 처리됨: $exception")
}
launch(handler) {
throw Exception("처리될 예외")
}
delay(100)
println("부모는 계속 실행됨")
}
fun useSupervisorJob() = runBlocking {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
val child1 = launch {
delay(100)
throw Exception("첫 번째 자식 실패")
}
val child2 = launch {
delay(200)
println("두 번째 자식은 정상 완료")
}
// child1의 실패가 child2에 영향을 주지 않음
joinAll(child1, child2)
}
}
suspend fun handlePartialFailure() = supervisorScope {
val results = mutableListOf<String>()
// 여러 작업을 병렬로 실행, 일부 실패해도 다른 작업은 계속
val jobs = List(5) { index ->
async {
delay(100)
if (index == 2) throw Exception("작업 $index 실패")
"작업 $index 완료"
}
}
// 각 작업의 결과를 개별적으로 처리
jobs.forEach { job ->
try {
results.add(job.await())
} catch (e: Exception) {
println("작업 실패: $e")
}
}
results
}