일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 티스토리챌린지
- 정보처리기사 실기 기출문제
- Linux
- MySQL
- kotlin
- minikube
- CKA
- Kubernetes
- 코틀린 코루틴의 정석
- aws
- 오블완
- kotlin coroutine
- APM
- Java
- CKA 기출문제
- Pinpoint
- AI
- AWS EKS
- 공부
- kotlin querydsl
- kotlin spring
- PETERICA
- mysql 튜닝
- Spring
- 기록으로 실력을 쌓자
- 정보처리기사 실기
- Elasticsearch
- CloudWatch
- 정보처리기사실기 기출문제
- IntelliJ
- Today
- Total
피터의 개발이야기
[kotlin] 코틀린 코루틴의 정석- 예외처리 본문
ㅁ 들어가며
ㅇ 코틀린 코루틴의 정석 책을 보고 정리한 글입니다.
8장 예외 처리
- 예외전파 방식
- 예외전파의 제한
- 예외를 CoroutineExcetionHandler 처리
- try catch문을 이용한 예외처리
- async를 통해 생성된 코루틴의 예외처리방법
- 전파되지 않는 예외
ㅁ 예외전파 방식
fun main() = runBlocking<Unit> {
launch(CoroutineName("작업1")) {
launch(CoroutineName("작업1-1")) {
delay(100L)
throw Exception("작업1-1 예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
launch(CoroutineName("작업2")) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
delay(1000L)
}
/*
[main @작업1#2] 작업1 실행
[main @작업2#3] 작업2 실행
Exception in thread "main" java.lang.Exception: 작업1-1 예외 발생
종료 코드 1(으)로 완료된 프로세스
*/
// exception 발생 시간을 50L로 축소
~~~
launch(CoroutineName("작업1-1")) {
delay(50L) //exception 발생 시간 조정
throw Exception("작업1-1 예외 발생")
}
~~~
/*
Exception in thread "main" java.lang.Exception: 작업1-1 예외 발생
종료 코드 1(으)로 완료된 프로세스
*/
ㅇ 코루틴에서 예외가 발생하면 부모코루틴에 전파되고 작업은 취소된다. 만약 예외처리가 되어 있으면 전체로 전파되지는 않는다. 예외전파로 인해 상위 코루틴이 취소가 되면, scope에 따라 전체가 취소처리 될 수 있다.
ㅇ 따라서 Exception 발생 시점에 따라 작업들의 진행 상태가 달라질 수 있다.
ㅁ 예외전파의 제한
fun main() = runBlocking<Unit> {
launch(CoroutineName("작업1")) {
launch(CoroutineName("작업1-1") + Job()) { // 새로운 Job을 연결
delay(50L)
throw Exception("작업1-1 예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
launch(CoroutineName("작업2")) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
delay(1000L)
}
/*
Exception in thread "main @작업1-1#4" java.lang.Exception: 작업1-1 예외 발생
[main @작업1#2] 작업1 실행
[main @작업2#3] 작업2 실행
종료 코드 0(으)로 완료된 프로세스
*/
ㅇ 예외 전파를 제한하기 위하여 새로운 Job을 연결하였다.
ㅇ 예외가 발생하여도 다른 작업들은 정상적으로 수행된다.
ㅇ 하지만 이 방법은 cancel시 또 다른 문제점이 될 수 있다.
Job을 이용한 예외전파의 한계
fun main() = runBlocking<Unit> {
val job1 = launch(CoroutineName("작업1")) {
launch(CoroutineName("작업1-1") + Job()) {
delay(100L)
// throw Exception("작업1-1 예외 발생")
println("[${Thread.currentThread().name}] 작업1-1 실행")
}
delay(100L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
val job2 = launch(CoroutineName("작업2")) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
delay(50L)
job1.cancel()
job2.cancel()
delay(1000L)
}
/*
[main @작업1-1#4] 작업1-1 실행
종료 코드 0(으)로 완료된 프로세스
*/
ㅇ Job을 분리시키면 작업이 분할되면서 cancel이 적용되지 않는다.
ㅇ Job을 이용한 예외전파의 한계를 극복하기 위해서는 SupervisorJob을 사용하면 된다.
SupervisorJob를 이용한 예외 전파 제한
fun main() = runBlocking<Unit> {
val supervisorJob = SupervisorJob() // 예외 전파를 제한하는 Job 선언
val job1 = launch(CoroutineName("작업1") + supervisorJob) {
launch(CoroutineName("작업1-1")) {
delay(50L)
throw Exception("작업1-1 예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
val job2 = launch(CoroutineName("작업2") + supervisorJob) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
delay(1000L)
}
/*
Exception in thread "main @작업1-1#4" java.lang.Exception: 작업1-1 예외 발생
[main @작업1#2] 작업1 실행
[main @작업2#3] 작업2 실행
종료 코드 0(으)로 완료된 프로세스
*/
ㅇ 이전 소스에서 Job-> SupervisorJob으로 변경하였다.
ㅇ 작업1-1이 예외가 발생하였지만, 예외가 전파되지 않고 다른 작업들이 수행되었다.
ㅇ SupervisorJob()을 생성 시 parent가 널이라 새로운 rootJob으로 이루어지기 때문에 cancel이 되지 않는 구조적 문제가 발생한다.
ㅇ 이를 방지 하기 위해서는 root의 Job을 주입시켜야 한다.
fun main() = runBlocking<Unit> {
val supervisorJob = SupervisorJob(this.coroutineContext[Job]) // 구도화된 동시성을 위해 상위 Job을 부모로 넘겨준다.
val job1 = launch(CoroutineName("작업1") + supervisorJob) {
launch(CoroutineName("작업1-1")) {
delay(100L)
throw Exception("작업1-1 예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
val job2 = launch(CoroutineName("작업2") + supervisorJob) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
supervisorJob.complete() // supervisorJob 완료처리를 해야한다.
}
/*
[main @작업1#2] 작업1 실행
[main @작업2#3] 작업2 실행
Exception in thread "main @작업1-1#4" java.lang.Exception: 작업1-1 예외 발생
종료 코드 0(으)로 완료된 프로세스
*/
ㅇ supervisorJob는 Job처럼 자동완료가 되지 않아 따로 명시적으로 완료처리를 해야한다.
SupervisorJob와 CoroutineScope를 이용한 예외제한
fun main() = runBlocking<Unit> {
val coroutineScope = CoroutineScope(SupervisorJob())
coroutineScope.apply {
launch(CoroutineName("작업1")) {
launch(CoroutineName("작업1-1")) {
throw Exception("작업1-1 예외 발생")
}
delay(200L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
launch(CoroutineName("작업2")) {
delay(200L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
}
delay(1000L)
}
/*
Exception in thread "DefaultDispatcher-worker-1 @작업1#2" java.lang.Exception: 작업1-1 예외 발생
...
[DefaultDispatcher-worker-1 @작업2#3] 작업2 실행
종료 코드 0(으)로 완료된 프로세스
*/
ㅇ 하나의 Scope로 묶어서 SupervisorJob을 한번 선언하면 예외전파를 제한할 수 있다.
SupervisorScope를 이용한 예외제한
fun main() = runBlocking<Unit> {
supervisorScope {
launch(CoroutineName("작업1")) {
launch(CoroutineName("작업1-1")) {
throw Exception("작업1-1 예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
launch(CoroutineName("작업2")) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
}
}
/*
Exception in thread "main @작업1#2" java.lang.Exception: 작업1-1 예외 발생
...
[main @작업2#3] 작업2 실행
종료 코드 0(으)로 완료된 프로세스
*/
ㅇ SupervisorScope 내부에서 실행되는 코루틴은 SupervisorJob과 부모자식으로 구조화되는데 supervisorScope의 SupervisorJob는 코드가 모두 실행되고 자식 코루틴도 모두 실행완료되면 자동으로 완료처리 된다.
ㅇ 그래서 supervisorScope를 사용하면 복잡한 설정 ㅇ벗이 구조화를 깨지 않고 예외 전파를 제한할 수 있다.
Job runBlocking |
|
SupervisorJob supervisorScope |
|
Job 작업1 |
Job 작업2 |
Job 작업1-1 |
ㅇ SupervisorScope 사용시 코루틴의 구조
ㅁ CoroutineExcetionHandler를 이용한 예외 처리
fun main() = runBlocking<Unit> {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] ${throwable}")
}
CoroutineScope(exceptionHandler).launch(CoroutineName("작업1")) {
throw Exception("작업1에 예외가 발생")
}
delay(1000L)
/*
[예외 발생] java.lang.Exception: 작업1에 예외가 발생
종료 코드 0(으)로 완료된 프로세스
*/
}
ㅇ CoroutineExceptionHandler가 마지막으로 전파되는 CoroutineScope의 CoroutineContext에 루트Job과 CoroutineExceptionHandler가 함께 설정돼 있었기 때문이다.
ㅇ 이처럼 CoroutineExceptionHandler를 마지막 전파위치에 설정하면 예외처리가 동작한다.
ㅇ CoroutineExceptionHandler는 예외를 전파하지 않는다.
ㅁ try catch문을 이용한 예외처리
fun main() = runBlocking<Unit> {
launch(CoroutineName("작업1")) {
try {
throw Exception("작업1에 예외가 발생했습니다")
} catch (e: Exception) {
println(e.message)
}
}
launch(CoroutineName("작업2")) {
delay(100L)
println("작업2 실행 완료")
}
}
/*
작업1에 예외가 발생했습니다
작업2 실행 완료
*/
ㅇtry catch문으로 작업1의 예외를 처리하여 작업2에 영향을 주지 않았다.
fun main() = runBlocking<Unit> {
try {
launch(CoroutineName("작업1")) {
// 새로운 코루틴에서 에러가 발생하는 경우 try가 catch할 수 없다.
throw Exception("작업1에 예외가 발생했습니다")
}
} catch (e: Exception) {
println(e.message)
}
launch(CoroutineName("작업2")) {
delay(100L)
println("작업2 실행 완료")
}
}
/*
Exception in thread "main" java.lang.Exception: 작업1에 예외가 발생했습니다
*/
ㅇ try catch는 같은 스래드 안에서 발생하는 경우만 catch가 가능하다.
ㅁ async의예외처리
fun main() = runBlocking<Unit> {
supervisorScope {
val deferred: Deferred<String> = async(CoroutineName("작업1")) {
throw Exception("작업1 예외 발생")
}
try {
deferred.await()
} catch (e: Exception) {
println("[예외] ${e.message}")
}
}
}
/*
Exception in thread "main" java.lang.Exception: 작업1에 예외가 발생했습니다
*/
// 취소전파
fun main() = runBlocking<Unit> {
async(CoroutineName("작업1")) {
throw Exception("작업1 예외 발생")
}
// 작업1에 의해 작업2는 취소된다.
launch(CoroutineName("작업2")) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
}
/*
Exception in thread "main" java.lang.Exception: 작업1 예외 발생
...
종료 코드 1(으)로 완료된 프로세스
*/
// 슈퍼바이저 예외 차단
fun main() = runBlocking<Unit> {
// 슈퍼바이저스코프를 설정하여 예외 전파를 차단한다.
supervisorScope {
async(CoroutineName("작업1")) {
throw Exception("작업1 예외 발생")
}
launch(CoroutineName("작업2")) {
delay(100L)
println("[${Thread.currentThread().name}] 작업2 실행")
}
}
}
/*
[main @작업2#3] 작업2 실행
*/
ㅁ 전파되지 않는 예외
fun main() = runBlocking<Unit>(CoroutineName("runBlocking 작업")) {
launch(CoroutineName("작업1")) {
launch(CoroutineName("작업2")) {
println("[${Thread.currentThread().name}] 작업2 실행")
throw CancellationException() //CancellationException 예외가 부모에게 전파되지 않는다
}
delay(100L)
println("[${Thread.currentThread().name}] 작업1 실행")
}
delay(100L)
println("[${Thread.currentThread().name}] runBlocking 작업 실행")
}
/*
[main @작업2#3] 작업2 실행
[main @runBlocking 작업#1] runBlocking 작업 실행
[main @작업1#2] 작업1 실행
종료 코드 0(으)로 완료된 프로세스
*/
ㅇ CancellationException는 예외가 부모에게 전파되지 않는다.
ㅇ 코루틴 취소 시 발생하는 JobCancellationException도 전파하지는 않는다. 상세 294p
ㅇ withTimeout으로 시간제약을 주는 경우 TimeoutCancellationException이 발생하지만 예외전파는 하지 않는다.
ㅁ 함께 보면 좋은 사이트
'Programming > Kotlin' 카테고리의 다른 글
[kotlin] 코틀린 코루틴의 정석 - 코루틴의 이해 (0) | 2024.06.07 |
---|---|
[kotlin] 코틀린 코루틴의 정석 - 일시 중단 함수 (0) | 2024.06.05 |
[kotlin] 코틀린 코루틴의 정석- 구조화된 동시성(Structured Concurrency) (0) | 2024.06.03 |
[kotlin] 코틀린 코루틴의 정석- CoroutineContext, 코루틴 실행환경 설정 (0) | 2024.06.02 |
[kotlin] 코틀린 코루틴의 정석- Async-await와 withContext 비교 (0) | 2024.06.01 |