스레드(Thread)

[Thread] 코루틴(Coroutine)의 예외처리

Stark97 2024. 9. 22. 15:02
반응형
 
 
 

코루틴의 예외처리 방법을 알아보자.

 

1. 테스트 환경

언어 Kotlin (Java21)
프레임워크 SpringBoot3.x.x
라이브러리 코루틴(core, reactor), retrofit, gson
IDE IntelliJ
AI tool ChatGPT 4o, Claude3.5 Sonnet
마음가짐 호기심, 참을성, 노력

SpringBoot: build.gradle 설정

  • 코루틴 설정과 retrofit 설정은 꼭 추가해 줘야만 끝까지 실습을 함께할 수 있다.
plugins {
	kotlin("jvm") version "1.9.25"
	kotlin("plugin.spring") version "1.9.25"
	id("org.springframework.boot") version "3.3.3"
	id("io.spring.dependency-management") version "1.1.6"
}

group = "com.study"
version = "0.0.1-SNAPSHOT"

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// spring boot
	implementation("org.springframework.boot:spring-boot-starter")
	implementation("org.jetbrains.kotlin:kotlin-reflect")

	// coroutines
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")

	// retrofit
	implementation("com.squareup.retrofit2:retrofit:2.11.0")
	implementation("com.squareup.retrofit2:converter-gson:2.11.0")
	implementation("com.google.code.gson:gson:2.11.0")


	// test
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
	compilerOptions {
		freeCompilerArgs.addAll("-Xjsr305=strict")
	}
}

tasks.withType<Test> {
	useJUnitPlatform()
}

 

2. 예외 처리 (Exception Handling)

코루틴에서의 예외 전파

  • 코루틴 내에서 발생한 예외는 코루틴의 스코프(scope)에 따라 전파된다. 코루틴의 스코프는 부모-자식 관계를 형성하며, 예외는 자식 코루틴에서 부모 코루틴으로 전파된다. 이는 구조화된 동시성(structured concurrency)의 핵심 개념으로, 예외 전파를 통해 코루틴의 실패가 전체 애플리케이션에 영향을 미치지 않도록 관리한다.

먼저, 예외가 어떻게 전파되는지 살펴보자.

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("메인 함수 시작")
    
    launch {
        println("코루틴 시작")
        throw Exception("코루틴 내에서 예외 발생")
    }
    
    println("메인 함수 계속 실행")
}

실행 결과

  • 예외가 발생하지만 catch 되지 않아 프로그램이 비정상 종료된다.
  • 예외는 launch(자식) 코루틴에서 runBlocking(부모) 코루틴으로 전파되어 최종적으로 메인 스레드를 중단시킨다.
메인 함수 시작
메인 함수 계속 실행
코루틴 시작
Exception in thread "main" java.lang.Exception: 코루틴 내에서 예외 발생
	at TestKt$main$1$1.invokeSuspend(Test.kt:8)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at TestKt.main(Test.kt:3)
	at TestKt.main(Test.kt)

코루틴 내부에서 예외 처리

  • 코루틴 내부에서 예외를 처리하면 예외의 전파를 막을 수 있다. (자식에서 부모로 전파되는 것을 막아보자)
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("메인 함수 시작")

    val job = launch {
        try {
            println("코루틴 시작")
            throw Exception("코루틴 내에서 예외 발생")
        } catch (e: Exception) {
            println("코루틴 내부에서 예외 처리: ${e.message}")
        }
    }

    job.join() // 코루틴이 완료될 때까지 대기
    println("메인 함수 계속 실행")
}

실행 결과

  • 코루틴 내부에서 예외를 catch 하여 처리한다.
  • 예외가 부모 코루틴으로 전파되지 않아 프로그램이 정상적으로 종료된다.
메인 함수 시작
코루틴 시작
코루틴 내부에서 예외 처리: 코루틴 내에서 예외 발생
메인 함수 계속 실행

주요 포인트

  • 예외 전파: 기본적으로 자식 코루틴에서 발생한 예외는 부모 코루틴으로 전파된다. (launch 내부에서 동작하는 자식 코루틴에서 발생한 예외가 부모 코루틴인 runBlocking으로 전파된다.)
  • 구조화된 동시성: 코루틴의 부모-자식 관계를 통해 예외 전파가 관리된다.
  • 예외 처리: 부모 코루틴에서 자식 코루틴의 예외를 처리할 수 있다.
  • 예외 처리 방법:
    • 코루틴 내부에서 try-catch를 사용하여 직접 처리
    • CoroutineExceptionHandler를 사용하여 전역적으로 처리 (다음 목차에서 설명)
  • 주의사항: runBlocking 내에서 발생한 예외는 프로그램을 중단시킬 수 있으므로, 적절한 예외 처리가 중요하다.

 

3. CoroutineExceptionHandler를 이용한 전역 예외 처리

  • CoroutineExceptionHandler는 전역적으로 예외를 처리할 수 있는 코루틴 예외 처리기다. 이를 사용하면 코루틴에서 발생한 예외를 모든 코루틴에 걸쳐 일괄적으로 처리할 수 있다. 특히, 상위 스코프에서 예외를 처리하지 못하는 경우 유용하게 사용할 수 있다.
import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("예외 처리기: ${exception.message}")
    }

    println("메인 함수 시작")

    val job = launch(handler) {
        println("코루틴 시작")
        throw Exception("코루틴 내에서 예외 발생")
    }

    job.join()
    println("메인 함수 종료")
}

코드 설명

  1. CoroutineExceptionHandler: 예외 발생 시 실행될 핸들러를 정의한다.
  2. launch(handler) {}: 예외 처리기를 지정하여 새로운 코루틴을 생성한다.
  3. throw Exception("코루틴 내에서 예외 발생!"): 코루틴 내에서 예외를 발생시킨다.
  4. job.join(): 코루틴의 완료를 대기한다.

실행 결과

메인 함수 시작
코루틴 시작
Exception in thread "main" java.lang.Exception: 코루틴 내에서 예외 발생
	at TestKt$main$1$job$1.invokeSuspend(Test.kt:12)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at TestKt.main(Test.kt:3)
	at TestKt.main(Test.kt)

 

 

 

!?!?! 출력을 보면 내가 작성한 "예외 처리기"가 제대로 동작하지 않는다.
그 이유는 다음과 같다.


 

runBlocking은 자체적으로 예외를 잡아서 다시 던지는 특성이 있어, CoroutineExceptionHandler가 예상대로 동작하지 않을 수 있다고 한다. (실제로 코드를 실행해 보면 제대로 동작하지 않는다.)

 

 

 

예외 처리기를 사용하도록 코드를 수정해 보자.

  • 이 수정된 버전에서는 다음과 같은 변경사항이 있다.
    1. GlobalScope.launch를 사용하여 코루틴을 생성한다. 이렇게 하면 코루틴이 runBlocking의 자식이 아닌 독립적인 코루틴으로 실행된다.
    2. CoroutineExceptionHandler를 GlobalScope.launch에 직접 전달한다.

이렇게 수정하면 예외가 CoroutineExceptionHandler에 의해 처리된다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("예외 처리기: ${exception.message}")
    }

    println("메인 함수 시작")

    // GlobalScope를 사용하여 최상위 코루틴 컨텍스트에서 실행
    val job = GlobalScope.launch(handler) {
        println("코루틴 시작")
        throw Exception("코루틴 내에서 예외 발생!")
    }

    job.join()
    println("메인 함수 종료")
}

출력은 다음과 같다.

  • 이 방식으로 CoroutineExceptionHandler가 예외를 잡아 처리하고, 메인 함수가 정상적으로 종료되는 것을 볼 수 있다.
메인 함수 시작
코루틴 시작
예외 처리기: 코루틴 내에서 예외 발생!
메인 함수 종료

주요 포인트

  • 전역 예외 처리: CoroutineExceptionHandler를 사용하여 전역적으로 예외를 처리할 수 있다.
  • 핸들러 등록: 코루틴 빌더(launch)에 예외 처리기를 등록하여 특정 코루틴의 예외를 처리할 수 있다.
  • 예외 전파: 핸들러가 등록된 코루틴에서 발생한 예외는 핸들러에서 처리된다.

CoroutineExceptionHandler의 한계 (async 빌더)

  • CoroutineExceptionHandler는 launch 빌더에만 적용된다. async 빌더에서는 예외가 결과를 통해 전파되므로, 핸들러가 예외를 잡지 못한다.
import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("예외 처리기: ${exception.message}")
    }

    val deferred = GlobalScope.async(handler) {
        println("코루틴 시작")
        throw Exception("예외 발생!")
    }

    try {
        deferred.await()
    } catch (e: Exception) {
        println("await에서 예외 잡힘: ${e.message}")
    }

    println("메인 함수 종료")
}

출력 결과

코루틴 시작
await에서 예외 잡힘: 예외 발생!
메인 함수 종료

여기서 주목할 점

  1. CoroutineExceptionHandler가 호출되지 않았다. ("예외 처리기"라는 출력이 된 적이 없다.)
  2. 대신, await()를 호출할 때 예외가 발생하여 catch 블록에서 잡혔다.

이는 async와 launch의 중요한 차이점을 보여준다.

  • launch는 "fire and forget" 스타일로, 예외가 즉시 전파되며 CoroutineExceptionHandler에서 처리될 수 있다.
  • async는 결과를 반환하는 것을 목적으로 하며, 예외는 결과의 일부로 간주된다. 따라서 예외는 await() 호출 시점까지 지연되며, CoroutineExceptionHandler가 동작하지 않는 것이다.

 

 

따라서 async를 사용할 때는 일반적으로 try-catch 구문을 사용하여 await() 호출 시 발생할 수 있는 예외를 처리해야 한다.

 

 

 

만약 async에서도 예외를 즉시 처리하고 싶다면, 다음과 같이 코루틴 내부에서 try-catch를 사용할 수 있다.

import kotlinx.coroutines.*


fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("예외 처리기: ${exception.message}")
    }

    val deferred = GlobalScope.async {
        try {
            println("코루틴 시작")
            throw Exception("예외 발생!")
        } catch (e: Exception) {
            println("코루틴 내부에서 예외 잡힘: ${e.message}")
        }
    }

    try {
        deferred.await()
    } catch (e: Exception) {
        println("await에서 예외 잡힘: ${e.message}")
    }

    println("메인 함수 종료")
}
  • 이렇게 하면 예외가 코루틴 내부에서 즉시 처리되며, await()를 호출할 때 예외가 전파되지 않는다.
코루틴 시작
코루틴 내부에서 예외 잡힘: 예외 발생!
메인 함수 종료

 

4. supervisorScope를 이용한 예외 격리

supervisorScope 사용하기

  • supervisorScope는 슈퍼바이저 스코프를 생성하여, 자식 코루틴의 실패가 다른 자식 코루틴이나 부모 코루틴에 영향을 미치지 않도록 한다. 이를 통해 안정적인 동시성 관리가 가능해진다.
  • 일반적인 coroutineScope와 달리, supervisorScope를 사용하면 자식 코루틴 중 하나가 실패해도 다른 자식 코루틴에는 영향을 주지 않는다.
import kotlinx.coroutines.*

fun main() = runBlocking {
    supervisorScope {
        val job1 = launch {
            delay(500L)
            throw Exception("코루틴 1 예외")
        }
        val job2 = launch {
            delay(1000L)
            println("코루틴 2 완료")
        }
        try {
            job1.join()
        } catch (e: Exception) {
            println("슈퍼바이저 스코프에서 예외 처리: ${e.message}")
        }
    }
    println("메인 함수 종료")
}

코드 설명

  1. supervisorScope {}: 슈퍼바이저 스코프를 생성하여 자식 코루틴의 예외 격리를 관리한다.
  2. launch {}: 두 개의 자식 코루틴을 생성한다.
    • job1: 500밀리 초 후에 예외를 발생시킨다.
    • job2: 1000밀리 초 후에 완료 메시지를 출력한다.
  3. try-catch 블록: job1의 예외를 잡아 처리한다.
  4. job1.join(): job1의 완료를 대기한다.

출력 결과

  • 코루틴 1만 예외가 발생하고 코루틴 2는 자신의 역할을 잘 수행하는 것을 확인할 수 있다.
Exception in thread "main" java.lang.Exception: 코루틴 1 예외
	at com.study.thread.study.coroutine.suspend.TestKt$main$1$1$job1$1.invokeSuspend(Test.kt:9)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:231)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:164)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:466)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:500)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:489)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:587)
	at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:490)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.study.thread.study.coroutine.suspend.TestKt.main(Test.kt:5)
	at com.study.thread.study.coroutine.suspend.TestKt.main(Test.kt)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@2fd66ad3, BlockingEventLoop@5d11346a]
코루틴 2 완료
메인 함수 종료

주요 포인트

  • 예외 격리: supervisorScope를 사용하면, 자식 코루틴의 예외가 다른 자식 코루틴에 영향을 미치지 않는다.
  • 안정적인 동시성: 일부 자식 코루틴이 실패하더라도, 나머지 자식 코루틴은 정상적으로 실행된다.
  • 예외 처리: try-catch 블록을 사용하여 개별 자식 코루틴의 예외를 처리할 수 있다.

supervisorScope와 CoroutineExceptionHandler의 결합

  • CoroutineExceptionHandler와 supervisorScope를 결합하여 예외를 전역적으로 처리하면서도 다른 자식 코루틴에 영향을 주지 않게 작성했다.
import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("전역 예외 처리기: ${exception.message}")
    }

    supervisorScope {
        val job1 = launch(handler) {
            delay(500L)
            throw Exception("코루틴 1 예외")
        }
        val job2 = launch {
            delay(1000L)
            println("코루틴 2 완료")
        }
        job1.join()
    }
    println("메인 함수 종료")
}

실행 결과

전역 예외 처리기: 코루틴 1 예외
코루틴 2 완료
메인 함수 종료

주요 포인트 (Key Points) 정리

  1. CoroutineExceptionHandler
    • 전역적으로 예외를 처리할 수 있는 방법을 제공한다.
    • 특정 코루틴에 예외 처리기를 설정하여 예외를 개별적으로 처리할 수 있다.

  2. supervisorScope
    • 자식 코루틴의 예외가 다른 자식 코루틴이나 부모 코루틴에 영향을 미치지 않도록 한다.
    • 안정적인 동시성 관리가 가능해진다.

  3. 예외 처리는 코루틴의 안정성과 신뢰성을 높이는 데 필수적이다.
    • 예외를 적절히 처리하지 않으면 애플리케이션의 예기치 않은 동작이나 충돌을 유발할 수 있다.

  4. 구조화된 동시성
    • 코루틴의 부모-자식 관계를 통해 예외 전파와 취소가 관리된다.
    • 이를 통해 코루틴의 수명 주기를 체계적으로 관리할 수 있다.

5. CoroutineScope와 supervisorScope의 비교

coroutineScope와 supervisorScope는 모두 코루틴 스코프를 생성하지만, 예외 전파 방식에서 차이가 있다.

  • 코드 예시를 통해 두 가지 예외 전파 방식을 비교해 보자.
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("coroutineScope 예제:")
    try {
        coroutineScope {
            launch {
                delay(500L)
                throw Exception("coroutineScope 내 코루틴 예외")
            }
            launch {
                delay(1000L)
                println("coroutineScope 내 두 번째 코루틴 완료")
            }
        }
    } catch (e: Exception) {
        println("coroutineScope에서 예외 처리: ${e.message}")
    }
    println("coroutineScope 예제 종료\n")

    println("supervisorScope 예제:")
    supervisorScope {
        launch {
            delay(500L)
            throw Exception("supervisorScope 내 코루틴 예외")
        }
        launch {
            delay(1000L)
            println("supervisorScope 내 두 번째 코루틴 완료")
        }
    }
    println("supervisorScope 예제 종료")
}

코드 설명

  1. coroutineScope {}
    • 자식 코루틴 중 하나가 예외를 발생시키면, 모든 자식 코루틴이 취소되고 예외가 상위 스코프로 전파된다.

  2. supervisorScope {}
    • 자식 코루틴 중 하나가 예외를 발생시켜도, 다른 자식 코루틴에는 영향을 미치지 않는다.
    • 예외는 스코프 외부로 전파되지 않고, 슈퍼바이저 스코프 내부에서만 처리된다.

실행 결과

coroutineScope 예제:
coroutineScope에서 예외 처리: coroutineScope 내 코루틴 예외
coroutineScope 예제 종료

supervisorScope 예제:
Exception in thread "main" java.lang.Exception: supervisorScope 내 코루틴 예외
	at com.study.thread.study.coroutine.suspend.TestKt$main$1$2$1.invokeSuspend(Test.kt:27)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:231)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:164)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:466)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:500)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:489)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:587)
	at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:490)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.study.thread.study.coroutine.suspend.TestKt.main(Test.kt:5)
	at com.study.thread.study.coroutine.suspend.TestKt.main(Test.kt)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@4516af24, BlockingEventLoop@4ae82894]
supervisorScope 내 두 번째 코루틴 완료
supervisorScope 예제 종료

주요 포인트

  1. coroutineScope
    • 자식 코루틴 중 하나가 실패하면, 모든 자식 코루틴이 취소되고, 예외가 상위 스코프로 전파된다.

  2. supervisorScope
    • 자식 코루틴의 실패가 다른 자식 코루틴에 영향을 미치지 않는다.
    • 예외는 스코프 내부에서만 처리되며, 슈퍼바이저 스코프 외부로 전파되지 않는다.

  3. 사용 사례:
    • coroutineScope: 자식 코루틴의 작업이 서로 의존적일 때 사용.
    • supervisorScope: 자식 코루틴의 작업이 독립적일 때 사용.

6. 실전 예외 처리 예제

네트워크 요청 시 예외 처리 (Handling Exceptions in Network Requests)

  • 네트워크 요청은 외부 요인으로 인해 예외가 발생할 가능성이 높다. 예를 들어, 서버 오류, 네트워크 연결 문제, 데이터 파싱 오류 등이 있다. 이러한 상황에서는 예외 처리를 통해 사용자에게 적절한 피드백을 제공하고, 애플리케이션의 안정성을 유지할 수 있다.

1. Retrofit을 이용한 네트워크 요청 예외 처리

  • jsonplaceholder를 사용해서 가짜 네트워크 요청을 보내도록 했다.
  • 네트워크 요청은 retrofit을 사용한다. (build.gradle 의존성 추가 필요)
 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake and reliable API for testing and prototyping. Powered by JSON Server + LowDB. Serving ~3 billion requests each month.

jsonplaceholder.typicode.com

import kotlinx.coroutines.*
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

// API 서비스 인터페이스 정의
interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<User>
}

// 데이터 클래스 정의
data class User(val id: Int, val name: String, val email: String)

fun main() = runBlocking {
    // Retrofit 인스턴스 생성
    val retrofit = Retrofit.Builder()
        .baseUrl("https://jsonplaceholder.typicode.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val service = retrofit.create(ApiService::class.java)

    // 네트워크 요청을 수행하는 코루틴
    val job = launch {
        try {
            val users = service.getUsers()
            users.forEach { user ->
                println("사용자: ${user.name}, 이메일: ${user.email}")
            }
        } catch (e: Exception) {
            println("API 호출 실패: ${e.message}")
        }
    }

    job.join()
    println("메인 함수 종료")
}

실행 결과

사용자: Leanne Graham, 이메일: Sincere@april.biz
사용자: Ervin Howell, 이메일: Shanna@melissa.tv
사용자: Clementine Bauch, 이메일: Nathan@yesenia.net
사용자: Patricia Lebsack, 이메일: Julianne.OConner@kory.org
사용자: Chelsey Dietrich, 이메일: Lucio_Hettinger@annie.ca
사용자: Mrs. Dennis Schulist, 이메일: Karley_Dach@jasper.info
사용자: Kurtis Weissnat, 이메일: Telly.Hoeger@billy.biz
사용자: Nicholas Runolfsdottir V, 이메일: Sherwood@rosamond.me
사용자: Glenna Reichert, 이메일: Chaim_McDermott@dana.io
사용자: Clementina DuBuque, 이메일: Rey.Padberg@karina.biz
메인 함수 종료

코드 설명

  1. Retrofit 설정: Retrofit.Builder를 사용하여 Retrofit 인스턴스를 생성하고, ApiService 인터페이스를 구현한다.
  2. 네트워크 요청: getUsers() 함수를 호출하여 사용자 데이터를 가져온다.
  3. 예외 처리: try-catch 블록을 사용하여 네트워크 요청 중 발생할 수 있는 예외를 처리한다.
    • 성공 시: 사용자 정보를 출력한다.
    • 실패 시: 예외 메시지를 출력한다.

주요 포인트

  1. 안정적인 네트워크 요청: 예외 처리를 통해 네트워크 오류 시 애플리케이션이 비정상적으로 종료되지 않도록 한다.
  2. 사용자 피드백: 실패 시 사용자에게 적절한 메시지를 제공하여 사용자 경험을 향상시킨다.
  3. 재시도 로직: 필요에 따라 네트워크 요청 실패 시 재시도 로직을 추가할 수 있다.

2. 사용자 입력 검증 예외 처리

  • 사용자 입력을 처리할 때도 예외가 발생할 수 있다. 예를 들어, 잘못된 형식의 입력, 필수 필드 누락 등이 있다. 이러한 예외를 처리하여 사용자에게 올바른 피드백을 제공하고, 애플리케이션의 안정성을 유지할 수 있다.
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            val userInput = getUserInput()
            validateInput(userInput)
            println("입력된 데이터: $userInput")
        } catch (e: InvalidInputException) {
            println("입력 오류: ${e.message}")
        } catch (e: Exception) {
            println("예기치 않은 오류: ${e.message}")
        }
    }

    job.join()
    println("메인 함수 종료")
}

// 사용자 입력을 받는 함수 (예시)
suspend fun getUserInput(): String {
    delay(500L) // 시뮬레이션
    return "홍길동" // 올바른 입력
    // return "" // 잘못된 입력 예시
}

// 사용자 입력을 검증하는 함수
fun validateInput(input: String) {
    if (input.isBlank()) {
        throw InvalidInputException("입력이 비어있습니다.")
    }
}

// 사용자 정의 예외 클래스
class InvalidInputException(message: String) : Exception(message)

실행 결과 (올바른 입력 시)

입력된 데이터: 홍길동
메인 함수 종료

실행 결과 (잘못된 입력 시)

  • 위의 코드에서 return부분 주석을 변경하면 테스트 가능하다.
입력 오류: 입력이 비어있습니다.
메인 함수 종료

코드 설명

  1. 사용자 입력받기: getUserInput() 함수를 통해 사용자로부터 입력을 받는다. 여기서는 시뮬레이션을 위해 delay를 사용한다.
  2. 입력 검증: validateInput(input) 함수를 통해 입력이 유효한지 검증한다. 유효하지 않은 경우 InvalidInputException을 던진다.
  3. 예외 처리: try-catch 블록을 사용하여 입력 검증 중 발생한 예외를 처리한다.
    • InvalidInputException: 사용자 입력이 잘못된 경우를 처리한다.
    • 기타 예외: 예상치 못한 예외를 처리한다.

만약 위에서 말하는 delay나 suspend fun이 뭔지 궁금하다면 아래의 코루틴에 대한 설명 포스팅을 읽어보자.

 

[Thread] 4. Kotlin의 코루틴(Coroutine)이란?

이번 편에서는 코루틴이 무엇인지 알아보자.📌 서론이전 스레드 시리즈에서는 '일반 스레드와 스레드풀'을 사용하여 성능 비교를 했었다.두가지 테스트만으로도 충분히 흥미로운 결과를 얻을

curiousjinan.tistory.com

 

주요 포인트

  • 사용자 경험 향상: 예외 처리를 통해 사용자에게 명확하고 친절한 피드백을 제공한다.
  • 입력 데이터 검증: 입력 데이터를 검증하여 애플리케이션의 안정성과 데이터 무결성을 유지한다.
  • 유연한 예외 처리: 다양한 예외 유형을 처리하여 코드의 유연성과 견고함을 높인다.

7. 예외 처리의 추가적인 고려사항

async 빌더에서의 예외 처리

  • async 빌더는 Deferred 객체를 반환하며, 결과를 await()할 때 예외가 발생한다. (이미 위의 목차에서 본 적이 있다.)
  • 이는 launch 빌더와는 다르게 예외가 즉시 전파되지 않는다. 따라서, async를 사용할 때는 await() 시점에 예외를 처리해야 한다.

1. async 빌더에서의 예외 처리

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred = async {
        println("async 코루틴 시작")
        throw Exception("async 코루틴 예외")
    }

    try {
        deferred.await()
    } catch (e: Exception) {
        println("async 예외 처리: ${e.message}")
    }

    println("메인 함수 종료")
}

실행 결과

async 코루틴 시작
async 예외 처리: async 코루틴 예외
메인 함수 종료
Exception in thread "main" java.lang.Exception: async 코루틴 예외
	at TestKt$main$1$deferred$1.invokeSuspend(Test.kt:6)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at TestKt.main(Test.kt:3)
	at TestKt.main(Test.kt)

주요 포인트

  • 예외 전파 시점: async 빌더에서 발생한 예외는 await() 호출 시점에 전파된다.
  • 예외 처리: try-catch 블록을 사용하여 await() 호출 시 발생한 예외를 처리할 수 있다.
  • CoroutineExceptionHandler의 영향: CoroutineExceptionHandler는 async 빌더에 의해 반환된 예외에 영향을 주지 않는다.

CoroutineScope와 예외 처리

  • 각 코루틴 빌더는 자신만의 예외 처리 방식을 가진다. 이를 이해하고 적절히 활용하는 것이 중요하다.
    1. launch: 예외가 발생하면 즉시 부모 스코프로 전파된다.
    2. async: 예외가 발생하면 await() 호출 시점에 전파된다.
    3. supervisorScope: 자식 코루틴의 예외가 다른 자식 코루틴에 영향을 미치지 않는다.
  •  
반응형