본문 바로가기

Language/Kotlin

[Kotlin] Coroutine 기초 과제

1. 빈칸 채우기 문제

 

1. 코루틴은 Kotlin에서 제공하는 경량 ________ 모델이다.

2. 코루틴의 핵심 3요소는 suspend fun, CoroutineScope, 그리고 ________ 이다.

3. suspend는 함수가 “항상 비동기”라는 뜻이 아니라, ________/재개 가능하다는 뜻이다.

4. 결과가 없는 비동기 작업을 시작할 때 주로 사용하는 빌더는 ________ 이다.

5. 결과가 있는 비동기 작업을 시작하고 await()로 받는 빌더는 ________ 이다.

6. 블로킹 I/O 작업은 보통 withContext(________)로 분리한다.

7. 자식 코루틴이 모두 끝날 때까지 기다리는 안전한 범위를 만드는 함수는 ________ 이다.

8. 자식 하나가 실패해도 다른 자식을 계속 수행하게 하는 범위는 ________ 이다.

9. 지정 시간 안에 끝나지 않으면 취소시키는 API는 with________ 이다.

10. 구조화된 동시성에서 부모가 취소되면 ________ 코루틴도 함께 취소된다.

11. 구조화되지 않은 비동기 작업이 요청 종료 후 떠도는 문제를 ________ task라고 부른다.

12. Kotlin은 예외 전파 중심, Go는 ________ 반환값 체크 중심의 에러 처리 스타일을 가진다.

13. Go의 context.WithTimeout에 대응되는 Kotlin 측 대표 패턴은 ________(ms) 이다.

14. 결제 시스템에서 재시도만 넣고 ________ 키를 빼면 중복 승인 위험이 커진다.

15. 코루틴 사용 시 가장 중요한 원칙 중 하나는 “모든 코루틴은 ________이 있어야 한다”이다.


정답

  1. 동시성
  2. Dispatcher
  3. 중단
  4. launch
  5. async
  6. Dispatchers.IO
  7. coroutineScope
  8. supervisorScope
  9. Timeout (withTimeout)
  10. 자식
  11. orphan
  12. error
  13. withTimeout
  14. 멱등성(idempotency)
  15. 소속(scope)

2. 핵심 개념( suspend, launch/async, withContext, coroutineScope, withTimeout)에 익숙해지도록 짧은 코드 채우기 문제 5개 + 정답 샘플

 

문제 1) suspend + 순차 실행

아래 코드를 완성해서 A 시작 -> A 끝 -> B 시작 -> B 끝 순서로 출력되게 하세요.

import kotlinx.coroutines.*

suspend fun stepA() {
    println("A 시작")
    ________(100)
    println("A 끝")
}

suspend fun stepB() {
    println("B 시작")
    ________(100)
    println("B 끝")
}

fun main() = runBlocking {
    ________()
    ________()
}

 

[정답 샘플]

import kotlinx.coroutines.*

suspend fun stepA() {
    println("A 시작")
    delay(100) // 코루틴을 중단하고 나중에 재개
    println("A 끝")
}

suspend fun stepB() {
    println("B 시작")
    delay(100)
    println("B 끝")
}

fun main() = runBlocking {
    stepA() // 순차 호출
    stepB() // 순차 호출
}

 

문제 2) async/await 병렬 실행

두 작업을 병렬로 실행해 총 소요시간을 줄이세요.

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun callRisk(): String {
    delay(200)
    return "RISK_OK"
}

suspend fun callPg(): String {
    delay(200)
    return "PG_OK"
}

fun main() = runBlocking {
    val elapsed = measureTimeMillis {
        val r1 = ________ { callRisk() }
        val r2 = ________ { callPg() }

        println("${r1.______()} + ${r2.______()}")
    }
    println("elapsed=${elapsed}ms")
}

 

[정답 샘플]

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun callRisk(): String {
    delay(200)
    return "RISK_OK"
}

suspend fun callPg(): String {
    delay(200)
    return "PG_OK"
}

fun main() = runBlocking {
    val elapsed = measureTimeMillis {
        val r1 = async { callRisk() } // 결과가 필요한 비동기 작업
        val r2 = async { callPg() }

        println("${r1.await()} + ${r2.await()}") // 결과 수집
    }
    println("elapsed=${elapsed}ms") // 보통 200ms대
}

 

문제 3) withContext(Dispatchers.IO) 분리

블로킹 I/O 성격의 함수를 IO 디스패처에서 실행하세요.

import kotlinx.coroutines.*

fun blockingDbRead(): String {
    Thread.sleep(100) // 블로킹 호출 가정
    return "order-123"
}

suspend fun findOrder(): String {
    return ________(________) {
        blockingDbRead()
    }
}

fun main() = runBlocking {
    println(findOrder())
}

 

[정답 샘플]

import kotlinx.coroutines.*

fun blockingDbRead(): String {
    Thread.sleep(100) // 블로킹 I/O 흉내
    return "order-123"
}

suspend fun findOrder(): String {
    return withContext(Dispatchers.IO) {
        // 블로킹 작업은 IO 풀에서 처리
        blockingDbRead()
    }
}

fun main() = runBlocking {
    println(findOrder())
}

 

문제 4) coroutineScope 구조화된 동시성

두 API를 병렬 호출하고 둘 다 성공해야 결과를 반환하게 하세요.

import kotlinx.coroutines.*

suspend fun riskCheck(): Boolean { delay(100); return true }
suspend fun limitCheck(): Boolean { delay(100); return true }

suspend fun validatePayment(): Boolean = ________ {
    val risk = ________ { riskCheck() }
    val limit = ________ { limitCheck() }

    risk.________() && limit.________()
}

fun main() = runBlocking {
    println(validatePayment())
}

 

[정답 샘플]

import kotlinx.coroutines.*

suspend fun riskCheck(): Boolean { delay(100); return true }
suspend fun limitCheck(): Boolean { delay(100); return true }

suspend fun validatePayment(): Boolean = coroutineScope {
    // 부모 scope 안에서 자식 코루틴을 구조적으로 관리
    val risk = async { riskCheck() }
    val limit = async { limitCheck() }

    risk.await() && limit.await()
}

fun main() = runBlocking {
    println(validatePayment())
}

 

문제 5) withTimeout 타임아웃 처리

외부 호출이 늦으면 타임아웃으로 실패하게 코드를 완성하세요.

import kotlinx.coroutines.*

suspend fun callSlowPg(): String {
    delay(1500)
    return "APPROVED"
}

fun main() = runBlocking {
    try {
        val result = ________(1000) {
            callSlowPg()
        }
        println(result)
    } catch (e: ________) {
        println("타임아웃 발생")
    }
}

 

[정답 샘플]

import kotlinx.coroutines.*

suspend fun callSlowPg(): String {
    delay(1500)
    return "APPROVED"
}

fun main() = runBlocking {
    try {
        val result = withTimeout(1000) {
            // 1초 안에 끝나지 않으면 취소
            callSlowPg()
        }
        println(result)
    } catch (e: TimeoutCancellationException) {
        println("타임아웃 발생")
    }
}

 


3. 결제 도메인 미니 프로젝트형(주문검증→리스크→승인)으로 합쳐서 한 파일에서 연습하기

// 파일명 예시: PaymentMiniPractice.kt
import kotlinx.coroutines.*
import kotlin.random.Random

data class Order(
    val orderId: String,
    val userId: String,
    val amount: Long
)

data class RiskResult(
    val isSafe: Boolean,
    val score: Int
)

enum class PaymentStatus {
    APPROVED, REJECTED
}

/** =========================
 *  1) 연습용 (TODO 채우기)
 *  ========================= */
suspend fun validateOrder(order: Order): Boolean {
    // TODO 1: amount가 0보다 크고, orderId/userId가 비어있지 않은지 검사
    return TODO("주문 검증 로직")
}

suspend fun runRiskCheck(order: Order): RiskResult = withContext(Dispatchers.IO) {
    delay(120) // 외부 리스크 API 가정
    val score = Random.nextInt(0, 100)
    RiskResult(isSafe = score < 70, score = score)
}

suspend fun callPgApprove(order: Order): Boolean = withContext(Dispatchers.IO) {
    delay(180) // PG 승인 API 가정
    true
}

suspend fun approvePayment(order: Order): PaymentStatus = withTimeout(1000) {
    // TODO 2: coroutineScope로 감싸기
    // TODO 3: async로 runRiskCheck / callPgApprove 병렬 실행
    // TODO 4: await 결과를 이용해 최종 승인/거절 결정
    TODO("승인 로직")
}

fun main() = runBlocking {
    val order = Order(orderId = "ORD-1001", userId = "USER-7", amount = 15000)

    try {
        val status = approvePayment(order)
        println("결제 결과: $status")
    } catch (e: TimeoutCancellationException) {
        println("결제 결과: TIMEOUT")
    }
}

 

정답

/** =========================
 *  2) 정답
 *  ========================= */
// 아래 정답은 연습 후 확인하세요.
/*
suspend fun validateOrder(order: Order): Boolean {
    // 주문 기본 유효성 검증
    return order.amount > 0 &&
        order.orderId.isNotBlank() &&
        order.userId.isNotBlank()
}

suspend fun approvePayment(order: Order): PaymentStatus = withTimeout(1000) {
    if (!validateOrder(order)) return@withTimeout PaymentStatus.REJECTED

    coroutineScope {
        // 리스크 검사와 PG 호출을 병렬로 수행
        val riskDeferred = async { runRiskCheck(order) }
        val pgDeferred = async { callPgApprove(order) }

        val risk = riskDeferred.await()
        val pgOk = pgDeferred.await()

        if (risk.isSafe && pgOk) PaymentStatus.APPROVED
        else PaymentStatus.REJECTED
    }
}
*/