본문으로 건너뛰기

⏸️ suspend 함수

📖 suspend란?

suspend 함수는 코루틴을 일시 중단할 수 있는 특별한 함수입니다. 쓰레드를 블록하지 않고 기다릴 수 있어서 효율적입니다!

💡 기본 개념

suspend 키워드

// suspend 함수는 다른 suspend 함수를 호출 가능
suspend fun doSomething() {
delay(1000) // suspend 함수
println("완료!")
}

fun main() = runBlocking {
doSomething() // 코루틴 안에서 호출
}

일반 함수 vs suspend 함수

// ❌ 일반 함수에서는 suspend 함수 호출 불가
fun normalFunction() {
// delay(1000) // 컴파일 에러!
}

// ✅ suspend 함수에서는 가능
suspend fun suspendFunction() {
delay(1000) // OK!
}

🎯 실전 예제

네트워크 요청

data class User(val id: String, val name: String)

suspend fun fetchUser(userId: String): User {
delay(1000) // 네트워크 지연 시뮬레이션
return User(userId, "홍길동")
}

fun main() = runBlocking {
println("요청 시작")
val user = fetchUser("user123")
println("받은 유저: ${user.name}")
}

순차 실행

suspend fun prepareFood(): String {
delay(1000)
return "음식"
}

suspend fun setTable(): String {
delay(500)
return "테이블"
}

suspend fun prepareMeal() {
println("준비 시작")

val food = prepareFood()
println("$food 준비 완료")

val table = setTable()
println("$table 준비 완료")

println("모든 준비 끝!")
}

fun main() = runBlocking {
val time = measureTimeMillis {
prepareMeal()
}
println("소요 시간: ${time}ms") // ~1500ms
}

병렬 실행

suspend fun prepareMealParallel() = coroutineScope {
println("준비 시작")

val foodDeferred = async { prepareFood() }
val tableDeferred = async { setTable() }

val food = foodDeferred.await()
val table = tableDeferred.await()

println("$food, $table 준비 완료")
}

fun main() = runBlocking {
val time = measureTimeMillis {
prepareMealParallel()
}
println("소요 시간: ${time}ms") // ~1000ms (병렬!)
}

🔧 withContext

디스패처 전환

suspend fun loadData(): String {
return withContext(Dispatchers.IO) {
// IO 쓰레드에서 실행
delay(1000)
"데이터"
}
}

fun main() = runBlocking {
println("메인 쓰레드: ${Thread.currentThread().name}")

val data = loadData()

println("다시 메인: ${Thread.currentThread().name}")
println("데이터: $data")
}

여러 컨텍스트

suspend fun processData() {
// IO 작업
val data = withContext(Dispatchers.IO) {
readFromFile()
}

// CPU 집약적 작업
val processed = withContext(Dispatchers.Default) {
heavyComputation(data)
}

// 메인 쓰레드 (UI 업데이트 등)
withContext(Dispatchers.Main) {
updateUI(processed)
}
}

suspend fun readFromFile(): String {
delay(500)
return "파일 내용"
}

suspend fun heavyComputation(data: String): String {
delay(1000)
return data.uppercase()
}

suspend fun updateUI(data: String) {
println("UI 업데이트: $data")
}

🎨 고급 패턴

조건부 대기

suspend fun fetchDataIfNeeded(cache: String?): String {
return cache ?: fetchFromNetwork()
}

suspend fun fetchFromNetwork(): String {
delay(1000)
return "새 데이터"
}

fun main() = runBlocking {
// 캐시 있음 - 즉시 반환
val cached = fetchDataIfNeeded("캐시 데이터")
println(cached)

// 캐시 없음 - 네트워크 요청
val fresh = fetchDataIfNeeded(null)
println(fresh)
}

체이닝

suspend fun step1(): Int {
delay(500)
return 10
}

suspend fun step2(input: Int): Int {
delay(500)
return input * 2
}

suspend fun step3(input: Int): Int {
delay(500)
return input + 5
}

suspend fun processChain(): Int {
val a = step1()
val b = step2(a)
val c = step3(b)
return c
}

fun main() = runBlocking {
val result = processChain()
println("최종 결과: $result") // 25 (10 * 2 + 5)
}

에러 핸들링

suspend fun riskyOperation(): String {
delay(1000)
throw Exception("네트워크 오류!")
}

suspend fun safeOperation(): String? {
return try {
riskyOperation()
} catch (e: Exception) {
println("에러 발생: ${e.message}")
null
}
}

fun main() = runBlocking {
val result = safeOperation()
println("결과: ${result ?: "실패"}")
}

🔥 실용 함수들

타임아웃 처리

suspend fun fetchWithTimeout(): String? {
return try {
withTimeout(2000) {
delay(3000) // 너무 오래 걸림
"데이터"
}
} catch (e: TimeoutCancellationException) {
println("시간 초과!")
null
}
}

fun main() = runBlocking {
val result = fetchWithTimeout()
println("결과: ${result ?: "타임아웃"}")
}

재시도 로직

suspend fun <T> retry(
times: Int = 3,
initialDelay: Long = 100,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
println("시도 ${attempt + 1} 실패: ${e.message}")
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong()
}
return block() // 마지막 시도
}

var attemptCount = 0

suspend fun unstableAPI(): String {
attemptCount++
if (attemptCount < 3) {
throw Exception("일시적 오류")
}
return "성공!"
}

fun main() = runBlocking {
val result = retry(times = 5) {
unstableAPI()
}
println(result)
}

캐시 패턴

class DataRepository {
private var cache: String? = null

suspend fun getData(forceRefresh: Boolean = false): String {
if (!forceRefresh && cache != null) {
println("캐시에서 반환")
return cache!!
}

println("네트워크에서 가져오는 중...")
delay(1000)
val data = "새 데이터"
cache = data
return data
}
}

fun main() = runBlocking {
val repo = DataRepository()

// 첫 호출 - 네트워크
println(repo.getData())

// 두 번째 호출 - 캐시
println(repo.getData())

// 강제 새로고침
println(repo.getData(forceRefresh = true))
}

🛡️ 취소 가능한 함수

취소 확인

suspend fun longRunningTask() {
repeat(10) { i ->
if (!isActive) {
println("취소됨!")
return
}

println("작업 $i")
delay(500)
}
}

fun main() = runBlocking {
val job = launch {
longRunningTask()
}

delay(2000)
println("작업 취소!")
job.cancelAndJoin()
}

ensureActive

suspend fun heavyWork() {
repeat(10) { i ->
ensureActive() // 취소되면 예외 발생

println("무거운 작업 $i")
Thread.sleep(500) // CPU 작업 시뮬레이션
}
}

fun main() = runBlocking {
val job = launch {
try {
heavyWork()
} catch (e: CancellationException) {
println("작업이 취소되었습니다")
}
}

delay(2000)
job.cancel()
}

🤔 자주 묻는 질문

Q1. suspend 함수는 쓰레드를 생성하나요?

A: 아닙니다! suspend는 쓰레드를 블록하지 않고 일시 중단만 합니다.

suspend fun example() {
println("시작: ${Thread.currentThread().name}")
delay(1000)
println("끝: ${Thread.currentThread().name}")
}
// 같은 쓰레드에서 실행될 수 있음!

Q2. 모든 함수를 suspend로 만들면?

A: 불필요합니다! 비동기 작업이 있을 때만 사용하세요.

// ❌ 불필요
suspend fun add(a: Int, b: Int) = a + b

// ✅ 필요
suspend fun fetchData() = withContext(Dispatchers.IO) {
// 네트워크 요청
}

Q3. suspend 함수에서 일반 함수 호출은?

A: 당연히 가능합니다!

fun normalFunction() = println("일반 함수")

suspend fun suspendFunction() {
normalFunction() // OK!
delay(1000)
}

🎬 마치며

suspend 함수로 효율적인 비동기 코드를!

핵심 정리:
✅ suspend로 일시 중단 가능
✅ 쓰레드 블록 없이 대기
✅ withContext로 쓰레드 전환
✅ 순차/병렬 실행 제어
✅ 취소 가능한 작업

다음 단계: Flow에서 비동기 데이터 스트림을 알아보세요!