| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 스프링
- Sort
- Effective Java
- effectivejava
- k8s
- ElasticSearch
- 알고리즘정렬
- Spring
- 김영한
- 자바
- JavaScript
- Kotlin
- 이펙티브 자바
- 자바스크립트
- Effective Java 3
- 알고리즘
- 예제로 배우는 스프링 입문
- 오블완
- 엘라스틱서치
- 스프링 핵심원리
- springboot
- 스프링핵심원리
- java
- 카카오
- 스프링부트
- kubernetes
- 티스토리챌린지
- 이차전지관련주
- 클린아키텍처
- 이펙티브자바
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin 코루틴 기초 | launch, async, suspend로 비동기 정복하기 본문
이 글을 읽으면: Thread의 복잡함 없이 Kotlin 코루틴으로 쉽고 안전하게 비동기 프로그래밍하는 방법을 배울 수 있습니다. 실무에서 바로 쓸 수 있는 launch, async, suspend 패턴을 실전 예제로 마스터하세요.
📌 목차
- 들어가며 - 왜 코루틴을 배워야 할까?
- 코루틴이란? - Thread와의 차이
- launch - 결과 없이 실행하기
- async/await - 결과 받아오기
- suspend 함수 - 일시 중단의 마법
- CoroutineScope와 Job
- 실전 비동기 패턴
- 마무리 - 다음 편 예고
들어가며 - 왜 코루틴을 배워야 할까?

실제 프로젝트에서 겪은 문제
2024년 9월, 신규 기능 개발 중
요구사항:
- 사용자 정보 조회 (DB, 300ms)
- 주문 내역 조회 (DB, 400ms)
- 추천 상품 조회 (API, 500ms)
→ 총 3개 작업을 동시에 처리하고 싶음
Thread 사용: 복잡하고 메모리 많이 먹음
CallbackHell: 코드가 너무 지저분함
RxJava: 러닝커브 너무 높음
해결책: Kotlin 코루틴 → 간단하고 직관적!
Thread의 문제점
Thread는 비싸다
// ❌ Thread 10만 개 만들기 - 불가능!
repeat(100_000) {
Thread {
Thread.sleep(1000)
println("작업 완료")
}.start()
}
// 결과: OutOfMemoryError 💥
비유로 이해하기
Thread = 직원 고용
- 직원 1명당 월급(메모리) 지급
- 10만 명 고용? → 회사 망함
Coroutine = 업무 분담
- 소수의 직원이 10만 개 업무를 번갈아 처리
- 효율적이고 경제적!
코루틴이 해결하는 것들
1. 간단한 문법
// Thread - 복잡함
Thread {
val result = fetchData()
runOnUiThread {
updateUI(result)
}
}.start()
// Coroutine - 간단함
launch {
val result = fetchData()
updateUI(result)
}
2. 메모리 효율
방식 10만 개 작업 메모리 사용
| Thread | 불가능 | 수 GB |
| Coroutine | 가능 | 수십 MB |
3. 구조적 동시성
부모 코루틴이 취소되면 자식도 자동 취소
→ 메모리 누수 방지
→ 앱 종료 시 안전
코루틴이란? - Thread와의 차이
코루틴의 핵심 개념
"일시 중단 가능한 경량 쓰레드"
// 이 함수는 "일시 중단"될 수 있다
suspend fun fetchUser(): User {
delay(1000) // 1초 대기 (Thread는 안 멈춤!)
return User("규철")
}
// 사용
launch {
println("시작")
val user = fetchUser() // 여기서 1초 대기
println("완료: ${user.name}")
}
일시 중단이 뭐길래?
일반 함수 (blocking):
Thread.sleep(1000)
→ 쓰레드가 멈춤 (다른 일 못 함)
suspend 함수 (non-blocking):
delay(1000)
→ 코루틴만 멈춤 (쓰레드는 다른 일 함)
비유: 요리하기
Thread 방식 (blocking):
파스타 삶는 동안 가만히 서서 기다림 ❌
→ 다른 요리 못 함
Coroutine 방식 (non-blocking):
파스타 삶는 동안 샐러드 만듦 ✅
→ 효율적!
실제 동작 원리
Thread는 1:1
// Thread 3개 = 작업 3개
repeat(3) {
Thread {
Thread.sleep(1000)
println("Thread $it 완료")
}.start()
}
// 실제 생성된 Thread: 3개
Coroutine은 N:M
// Thread 1개 = 작업 10만 개
repeat(100_000) {
launch {
delay(1000)
println("Coroutine $it 완료")
}
}
// 실제 생성된 Thread: CPU 코어 수만큼 (예: 4개)
launch - 결과 없이 실행하기
launch의 기본 사용법
"그냥 실행만 하면 돼, 결과는 필요 없어"
import kotlinx.coroutines.*
fun main() = runBlocking { // 메인 함수를 코루틴으로
println("시작")
launch {
delay(1000) // 1초 대기
println("World!")
}
println("Hello")
delay(2000) // 코루틴이 끝날 때까지 기다림
}
// 출력:
// 시작
// Hello
// (1초 후)
// World!
왜 Hello가 먼저 나올까?
1. launch는 "나중에 실행"을 예약
2. 메인 코드는 계속 실행
3. delay(1000) 동안 대기
4. 시간 되면 실행
실전 예제: 로깅 시스템
문제 상황
// ❌ 동기 방식 - 로그 쓰는 동안 멈춤
fun processOrder(order: Order) {
saveLog("주문 시작: ${order.id}") // 500ms 걸림 ⏱️
validateOrder(order)
saveLog("주문 검증 완료") // 500ms 걸림 ⏱️
processPayment(order)
saveLog("결제 완료") // 500ms 걸림 ⏱️
}
// 총 시간: 원래 작업 + 1500ms 😢
코루틴으로 해결
// ✅ 비동기 방식 - 로그는 백그라운드에서
fun processOrder(order: Order) = runBlocking {
launch { saveLog("주문 시작: ${order.id}") } // 백그라운드
validateOrder(order)
launch { saveLog("주문 검증 완료") } // 백그라운드
processPayment(order)
launch { saveLog("결제 완료") } // 백그라운드
}
// 총 시간: 원래 작업만! 😊
여러 개 동시 실행
순차 실행 vs 동시 실행
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
// ❌ 순차 실행 - 느림
println("=== 순차 실행 ===")
delay(1000)
delay(1000)
delay(1000)
println("소요 시간: ${System.currentTimeMillis() - startTime}ms")
// 소요 시간: 3000ms
}
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
// ✅ 동시 실행 - 빠름
println("=== 동시 실행 ===")
launch { delay(1000); println("작업 1 완료") }
launch { delay(1000); println("작업 2 완료") }
launch { delay(1000); println("작업 3 완료") }
delay(1500) // 모두 끝날 때까지 대기
println("소요 시간: ${System.currentTimeMillis() - startTime}ms")
// 소요 시간: 1500ms (3배 빠름!)
}
async/await - 결과 받아오기
launch vs async 차이
간단 비교
launch async
| 반환값 | Job (완료 여부만) | Deferred<T> (결과값) |
| 용도 | 로깅, 업데이트 등 | 계산, API 호출 등 |
| 비유 | "청소해" (끝났는지만 확인) | "계산해" (결과 받기) |
async 기본 사용법
fun main() = runBlocking {
val deferred = async {
delay(1000)
42 // 결과값
}
println("계산 중...")
val result = deferred.await() // 결과 기다림
println("결과: $result")
}
// 출력:
// 계산 중...
// (1초 후)
// 결과: 42
실전 예제: 병렬 API 호출
문제 상황
// ❌ 순차 호출 - 느림
fun loadDashboard() = runBlocking {
val user = fetchUser() // 300ms
val orders = fetchOrders() // 400ms
val products = fetchProducts() // 500ms
Dashboard(user, orders, products)
}
// 총 시간: 1200ms 😢
async로 병렬 처리
// ✅ 병렬 호출 - 빠름
fun loadDashboard() = runBlocking {
val userDeferred = async { fetchUser() } // 동시 시작
val ordersDeferred = async { fetchOrders() } // 동시 시작
val productsDeferred = async { fetchProducts() } // 동시 시작
val user = userDeferred.await() // 결과 기다림
val orders = ordersDeferred.await()
val products = productsDeferred.await()
Dashboard(user, orders, products)
}
// 총 시간: 500ms (가장 긴 작업 시간) 😊
실제 측정
data class User(val name: String)
data class Order(val id: Int)
data class Product(val name: String)
data class Dashboard(val user: User, val orders: List<Order>, val products: List<Product>)
suspend fun fetchUser(): User {
delay(300)
return User("규철")
}
suspend fun fetchOrders(): List<Order> {
delay(400)
return listOf(Order(1), Order(2))
}
suspend fun fetchProducts(): List<Product> {
delay(500)
return listOf(Product("노트북"), Product("마우스"))
}
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val dashboard = loadDashboard()
println("소요 시간: ${System.currentTimeMillis() - startTime}ms")
println("대시보드: $dashboard")
}
// 출력:
// 소요 시간: 약 500ms
// 대시보드: Dashboard(user=User(name=규철), ...)
async의 함정 - await()을 바로 호출하면?
// ❌ 나쁜 예 - async 의미 없음
fun loadDashboard() = runBlocking {
val user = async { fetchUser() }.await() // 바로 await
val orders = async { fetchOrders() }.await() // 순차 실행됨!
val products = async { fetchProducts() }.await()
Dashboard(user, orders, products)
}
// 총 시간: 1200ms (순차 실행과 동일)
올바른 사용법
// ✅ 좋은 예 - 먼저 시작, 나중에 await
fun loadDashboard() = runBlocking {
val userDeferred = async { fetchUser() }
val ordersDeferred = async { fetchOrders() }
val productsDeferred = async { fetchProducts() }
// 모두 시작된 후에 await
Dashboard(
userDeferred.await(),
ordersDeferred.await(),
productsDeferred.await()
)
}
suspend 함수 - 일시 중단의 마법
suspend가 뭐길래?
suspend = "일시 중단 가능한"
// 일반 함수
fun normalFunction() {
Thread.sleep(1000) // 쓰레드 멈춤 (다른 일 못 함)
}
// suspend 함수
suspend fun suspendFunction() {
delay(1000) // 코루틴만 멈춤 (쓰레드는 다른 일 함)
}
비유: 전화 통화
일반 함수 (blocking):
통화 중에는 아무것도 못 함 ❌
suspend 함수 (non-blocking):
통화 중에 문자 확인, 메모 작성 가능 ✅
suspend 함수 만들기
규칙: suspend 함수 안에서만 호출 가능
suspend fun fetchUser(): User {
delay(1000) // suspend 함수는 delay 호출 가능
return User("규철")
}
fun main() {
// fetchUser() // ❌ 에러! 일반 함수에서 호출 불가
// ✅ 코루틴 안에서만 가능
runBlocking {
val user = fetchUser()
println(user)
}
}
실전 예제: 데이터베이스 작업
// Repository 패턴
class UserRepository {
// suspend 함수로 선언
suspend fun findById(id: Long): User? {
delay(300) // DB 조회 시뮬레이션
return User("규철")
}
suspend fun save(user: User) {
delay(500) // DB 저장 시뮬레이션
println("사용자 저장 완료: ${user.name}")
}
}
// Service 레이어
class UserService(private val repository: UserRepository) {
suspend fun createUser(name: String): User {
val user = User(name)
repository.save(user)
return user
}
suspend fun getUserWithOrders(id: Long): UserWithOrders {
// 동시 조회
val userDeferred = async { repository.findById(id) }
val ordersDeferred = async { fetchOrders(id) }
return UserWithOrders(
user = userDeferred.await()!!,
orders = ordersDeferred.await()
)
}
}
suspend의 장점
1. 체이닝이 자연스럽다
suspend fun processUser(userId: Long) {
val user = fetchUser(userId) // 1단계
val profile = fetchProfile(user) // 2단계
val orders = fetchOrders(user) // 3단계
updateDashboard(user, profile, orders)
}
// 마치 동기 코드처럼 읽힘!
2. 예외 처리가 간단하다
suspend fun safeProcess(userId: Long) {
try {
val user = fetchUser(userId)
processUser(user)
} catch (e: Exception) {
println("에러 발생: ${e.message}")
}
}
// try-catch 그대로 사용 가능!
CoroutineScope와 Job
CoroutineScope가 뭐길래?
"코루틴의 생명주기를 관리하는 범위"
// ❌ 잘못된 예 - 메모리 누수
fun startBackgroundWork() {
GlobalScope.launch { // 앱 종료까지 살아있음
while (true) {
delay(1000)
println("작업 중...")
}
}
}
// 문제: 화면 나가도 계속 실행됨!
비유: 프로젝트 팀
CoroutineScope = 프로젝트
- 프로젝트 종료되면 팀원(코루틴) 모두 해산
GlobalScope = 평생 고용
- 회사 망할 때까지 일함
Scope 종류
Scope 생명주기 용도
| GlobalScope | 앱 전체 | ❌ 거의 사용 금지 |
| CoroutineScope | 직접 관리 | ✅ 일반적 사용 |
| viewModelScope | ViewModel | ✅ Android |
| lifecycleScope | Activity/Fragment | ✅ Android |
Job으로 제어하기
Job = "작업 제어권"
fun main() = runBlocking {
val job = launch {
repeat(10) {
delay(500)
println("작업 $it")
}
}
delay(2000)
job.cancel() // 작업 취소
println("취소됨")
}
// 출력:
// 작업 0
// 작업 1
// 작업 2
// 취소됨
실전 예제: Activity 생명주기
문제 상황
// ❌ 나쁜 예 - Activity 종료 후에도 실행
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalScope.launch {
while (true) {
delay(1000)
updateUI() // 💥 Activity 종료 후 크래시!
}
}
}
}
올바른 방법
// ✅ 좋은 예 - Scope로 관리
class MainActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
scope.launch {
while (true) {
delay(1000)
updateUI()
}
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel() // Activity 종료 시 자동 취소
}
}
구조적 동시성
"부모가 취소되면 자식도 취소"
fun main() = runBlocking {
val parentJob = launch {
println("부모 시작")
launch {
delay(3000)
println("자식 1 완료") // 실행 안 됨
}
launch {
delay(3000)
println("자식 2 완료") // 실행 안 됨
}
delay(1000)
println("부모 완료")
}
delay(500)
parentJob.cancel() // 부모 취소 → 자식도 자동 취소
println("모두 취소됨")
}
// 출력:
// 부모 시작
// 모두 취소됨
실전 패턴 모음
패턴 1: 타임아웃 처리
"3초 안에 응답 없으면 실패"
suspend fun fetchWithTimeout(): User? {
return withTimeout(3000) { // 3초 제한
fetchUser()
}
}
// 사용
fun main() = runBlocking {
try {
val user = fetchWithTimeout()
println("성공: $user")
} catch (e: TimeoutCancellationException) {
println("타임아웃!")
}
}
패턴 2: 진행 상황 업데이트
"다운로드하면서 진행률 표시"
suspend fun downloadFile(
url: String,
onProgress: (Int) -> Unit
) {
repeat(100) { progress ->
delay(50) // 다운로드 시뮬레이션
onProgress(progress + 1)
}
}
// 사용
fun main() = runBlocking {
downloadFile("https://example.com/file.zip") { progress ->
println("진행률: $progress%")
}
}
패턴 3: 재시도 로직
"실패하면 3번까지 재시도"
suspend fun <T> retryWithDelay(
times: Int = 3,
initialDelay: Long = 100,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: Exception) {
println("실패, ${currentDelay}ms 후 재시도")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong()
}
}
return block() // 마지막 시도
}
// 사용
fun main() = runBlocking {
val result = retryWithDelay {
fetchUser() // 네트워크 요청
}
println("결과: $result")
}
패턴 4: 동시 실행 수 제한
"한 번에 최대 3개씩만 실행"
import kotlinx.coroutines.channels.Channel
suspend fun processWithLimit(
items: List<String>,
limit: Int = 3,
process: suspend (String) -> Unit
) {
val channel = Channel<Unit>(limit)
items.forEach { item ->
channel.send(Unit) // 슬롯 확보
launch {
try {
process(item)
} finally {
channel.receive() // 슬롯 반환
}
}
}
}
마무리 - 다음 편 예고
오늘 배운 것 ✅
- 코루틴 개념 - 경량 쓰레드, 일시 중단
- launch - 결과 없이 실행 (Fire and Forget)
- async/await - 결과값 받기 (병렬 처리)
- suspend - 일시 중단 가능한 함수
- Scope와 Job - 생명주기 관리
- 실전 패턴 - 타임아웃, 재시도, 진행 상황
다음 편에서 배울 것 📚
15편: 코루틴 심화 | Flow, Channel로 데이터 스트림 다루기
- Flow가 뭐길래?
- cold vs hot 스트림
- collect, map, filter 연산자
- StateFlow, SharedFlow
- Channel로 통신하기
- 실전 반응형 프로그래밍
핵심 정리
코루틴 3원칙
- 결과 필요 없으면 launch
- 결과 필요하면 async + await
- 항상 Scope 관리 (cancel 잊지 말기)
코루틴 vs Thread
Thread Coroutine
| 생성 비용 | 비쌈 (1MB) | 저렴 (수 KB) |
| 개수 제한 | 수천 개 | 수십만 개 |
| 문법 | 복잡 | 간단 |
| 비유 | 직원 고용 | 업무 분담 |
💬 댓글로 알려주세요!
- 코루틴을 어디에 사용해보셨나요?
- Thread에서 코루틴으로 바꾼 경험이 있나요?
- 이 글이 도움이 되셨나요?
태그: #Kotlin #코루틴 #Coroutine #launch #async #suspend #비동기
'개발 > java basic' 카테고리의 다른 글
| Kotlin 코루틴 심화 | Flow, Channel로 데이터 스트림 다루기 (0) | 2026.01.01 |
|---|---|
| Kotlin 예외 처리 완벽 가이드 | try-catch, runCatching, Result로 안전한 코드 만들기 (0) | 2025.12.28 |
| Kotlin Delegation 완벽 가이드 | by 키워드로 보일러플레이트 제거하기 (0) | 2025.12.28 |
| Kotlin 상속과 인터페이스 완벽 가이드 | open, abstract, sealed (1) | 2025.12.27 |
| Kotlin 제네릭스 완벽 가이드 | in, out, reified 마스터하기 (0) | 2025.12.27 |
