본문으로 건너뛰기

🎯 코루틴 기초

📖 코루틴 시작하기

코루틴을 사용하려면 먼저 코루틴 빌더를 알아야 합니다. 가장 기본적인 세 가지 빌더를 알아봅시다!

💡 runBlocking

기본 사용법

import kotlinx.coroutines.*

fun main() = runBlocking { // 코루틴 시작!
println("Hello")
delay(1000) // 1초 대기
println("World!")
}

특징

fun main() {
println("Before")

runBlocking {
delay(1000)
println("Inside coroutine")
}

println("After") // 코루틴이 끝날 때까지 대기
}
// Before
// (1초 대기)
// Inside coroutine
// After

주의: runBlocking은 현재 쓰레드를 블록합니다! 주로 테스트main 함수에서만 사용하세요.

🚀 launch

비동기 작업 시작

fun main() = runBlocking {
launch {
delay(1000)
println("Task 1")
}

launch {
delay(500)
println("Task 2")
}

println("Main")
}
// Main
// (500ms 후) Task 2
// (1000ms 후) Task 1

Job 반환

fun main() = runBlocking {
val job = launch {
repeat(5) { i ->
println("Working... $i")
delay(500)
}
}

delay(1300)
println("작업 취소!")
job.cancel() // 작업 취소
job.join() // 취소 완료 대기
}

여러 작업 관리

fun main() = runBlocking {
val jobs = List(5) { i ->
launch {
delay(1000L * i)
println("작업 $i 완료")
}
}

jobs.forEach { it.join() } // 모든 작업 대기
println("모든 작업 끝!")
}

⚡ async

결과 반환받기

fun main() = runBlocking {
val deferred = async {
delay(1000)
return@async "결과값"
}

println("작업 중...")
val result = deferred.await() // 결과 대기
println("받은 값: $result")
}

launch vs async

fun main() = runBlocking {
// launch - 결과 없음
launch {
delay(1000)
println("launch 완료")
}

// async - 결과 반환
val result = async {
delay(1000)
"async 완료"
}

println(result.await())
}

🎯 실전 예제

병렬 처리

import kotlin.system.measureTimeMillis

fun main() = runBlocking {
val time = measureTimeMillis {
val one = async { fetchData1() }
val two = async { fetchData2() }

println("결과: ${one.await()}, ${two.await()}")
}

println("소요 시간: ${time}ms") // ~1000ms (병렬 실행)
}

suspend fun fetchData1(): String {
delay(1000)
return "데이터1"
}

suspend fun fetchData2(): String {
delay(1000)
return "데이터2"
}

순차 vs 병렬

fun main() = runBlocking {
// ❌ 순차 실행 (느림)
val time1 = measureTimeMillis {
val one = fetchData1()
val two = fetchData2()
println("$one, $two")
}
println("순차: ${time1}ms") // ~2000ms

// ✅ 병렬 실행 (빠름)
val time2 = measureTimeMillis {
val one = async { fetchData1() }
val two = async { fetchData2() }
println("${one.await()}, ${two.await()}")
}
println("병렬: ${time2}ms") // ~1000ms
}

예외 처리

fun main() = runBlocking {
val deferred = async {
delay(1000)
throw Exception("에러 발생!")
}

try {
deferred.await()
} catch (e: Exception) {
println("잡은 에러: ${e.message}")
}
}

🔥 실용 패턴

타임아웃

fun main() = runBlocking {
try {
withTimeout(1300) {
repeat(3) { i ->
println("작업 $i")
delay(500)
}
}
} catch (e: TimeoutCancellationException) {
println("시간 초과!")
}
}

여러 API 호출

data class UserData(
val profile: String,
val posts: List<String>,
val friends: List<String>
)

suspend fun loadUserData(userId: String): UserData = coroutineScope {
val profile = async { fetchProfile(userId) }
val posts = async { fetchPosts(userId) }
val friends = async { fetchFriends(userId) }

UserData(
profile.await(),
posts.await(),
friends.await()
)
}

suspend fun fetchProfile(id: String): String {
delay(500)
return "Profile($id)"
}

suspend fun fetchPosts(id: String): List<String> {
delay(800)
return listOf("Post1", "Post2")
}

suspend fun fetchFriends(id: String): List<String> {
delay(600)
return listOf("Friend1", "Friend2")
}

재시도 로직

suspend fun <T> retryIO(
times: Int = 3,
delay: Long = 1000,
block: suspend () -> T
): T {
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
println("시도 ${attempt + 1} 실패")
delay(delay)
}
}
return block() // 마지막 시도
}

fun main() = runBlocking {
val result = retryIO(times = 3) {
fetchDataFromAPI()
}
println(result)
}

var attempt = 0
suspend fun fetchDataFromAPI(): String {
attempt++
if (attempt < 3) {
throw Exception("네트워크 오류")
}
return "성공!"
}

🛠️ coroutineScope

구조화된 동시성

suspend fun doWork() = coroutineScope {
launch {
delay(1000)
println("작업 1")
}

launch {
delay(2000)
println("작업 2")
}

println("모든 작업 시작")
// 모든 자식이 끝날 때까지 대기
}

fun main() = runBlocking {
doWork()
println("완료!")
}

예외 전파

suspend fun riskyWork() = coroutineScope {
launch {
delay(500)
throw Exception("에러!")
}

launch {
delay(1000)
println("이 코드는 실행 안 됨")
}
}

fun main() = runBlocking {
try {
riskyWork()
} catch (e: Exception) {
println("에러 잡음: ${e.message}")
}
}

🤔 자주 묻는 질문

Q1. launch와 async의 차이는?

A: 결과 반환 유무!

fun main() = runBlocking {
// launch - 결과 필요 없음
launch {
println("단순 작업")
}

// async - 결과 필요
val result = async {
"반환할 값"
}
println(result.await())
}

Q2. join()과 await()의 차이는?

A: join은 완료 대기, await은 결과 대기!

fun main() = runBlocking {
val job = launch {
delay(1000)
}
job.join() // 완료만 대기

val deferred = async {
delay(1000)
"결과"
}
val result = deferred.await() // 결과 대기
}

Q3. 코루틴은 자동으로 취소되나요?

A: 부모가 취소되면 자식도 취소됩니다!

fun main() = runBlocking {
val parent = launch {
val child = launch {
repeat(10) {
println("자식 작업 $it")
delay(500)
}
}

delay(1300)
println("부모 완료")
}

delay(2000)
parent.cancel() // 자식도 함께 취소됨
}

🎬 마치며

코루틴의 기본 빌더를 마스터했습니다!

핵심 정리:
✅ runBlocking - 테스트/main 함수용
✅ launch - 결과 필요 없는 작업
✅ async - 결과 반환받기
✅ coroutineScope - 구조화된 동시성
✅ 병렬 처리로 성능 향상

다음 단계: suspend 함수에서 일시 중단 함수를 깊이 알아보세요!