코틀린 코루틴 (10장 정리 - 예외 처리)

윤성현·2024년 12월 20일

코틀린 코루틴

목록 보기
10/11
post-thumbnail

10장. 예외 처리

기본 빌더에서 예외가 자식에서 부모로 전파되는 것을 이해하고 어떻게 멈추는지 이해해보자! ⚡

요약 📜

  • 기본 코루틴 빌더에서는 자식 코루틴의 예외가 부모로 전파되며, 부모가 취소되면 자식도 취소되는 구조화된 동시성이 이루어짐.
  • 일반 Job을 사용할 경우, 자식의 예외가 부모를 멈추게 하여 전체 흐름이 종료될 수 있음.
  • SupervisorJob 또는 supervisorScope를 사용하면 한 자식의 예외가 다른 자식과 부모에게 영향을 주지 않도록 할 수 있음.
  • CancellationException 계열 예외는 부모 전파 없이 해당 코루틴만 취소시킴.
  • CoroutineExceptionHandler를 사용해 전파된 예외에 대한 공통 처리 로직을 추가할 수 있음.

서론

  • 핵심 개념: 일반적인 코루틴 빌더(runBlocking, launch, async)에서 예외가 발생하면, 해당 예외는 부모 코루틴으로 전파되고, 부모가 취소되면 자식들도 함께 취소됨
  • 예외 발생 → 부모 종료 → 자식들도 취소되는 구조화된 동시성 패턴이 기본 동작
fun main(): Unit = runBlocking {
	launch {
		launch {
			delay(1000)
			throw Error("Some error")
		}
		
		launch {
			delay(2000)
			println("Will not be printed")
		}
		
		launch {
			delay(500) // 예외 발생보다 빠름
			println("Will be printed")
		}
	}
	
	launch {
		delay(2000)
		println("Will not be printed")
	}
}
// Will be printed
// Exception in thread "main" java.lang.Error: Some error...
  • 예외는 자식에서 부모로 전파되고, 부모가 취소되면 자식도 취소되어 쌍방으로 전파됨

코루틴 종료 멈추기 ⛔

  • 코루틴 간의 상호작용은 잡을 통해서 일어나며, 코루틴 빌더 내부에서 새로운 코루틴 빌더를 try-catch문을 통해 래핑하는 건 도움이 되지 않음
fun main(): Unit = runBlocking {
	try { // 래핑해도 무시됨
		launch {
			delay(1000)
			throw Error("Some error")
		}
	} catch (e: Throwable) { // 아무 도움도 되지 않음
		println("Will not be printed")
	}
	
	launch {
		delay(2000)
		println("Will not be printed")
	}
}
// Exception in thread "main" java.lang.Error: Some error...

SupervisorJob

  • SupervisorJob을 사용하면 자식에서 발생한 모든 예외를 무시할 수 있음

일반적으로 SupervisorJob은 다수의 코루틴을 시작하는 스코프로 사용됨

fun main(): Unit = runBlocking {
	val scope = CoroutineScope(SupervisorJob())
	scope.launch {
		delay(1000)
		throw Error("Some error")
	} 
	
	scope.launch {
		delay(2000)
		println("Will be printed")
	}
	delay(3000)
}
// Exception ...
// Will be printed

‼️ SupervisorJob을 부모 코루틴의 인자로 사용하지 마십시오. 이 역시 아무런 도움이 되지 않습니다.

fun main(): Unit = runBlocking {
	// 잘못된 예
	// 자식 코루틴 하나가 있고, 부모 코루틴이 없는 잡은 일반 잡과 동일하게 작동
	launch(SupervisorJob()) {
		launch {
			delay(1000)
			throw Error("Some error")
		}
	} 
	
	launch {
		delay(2000)
		println("Will not be printed")
	}
	
	delay(3000)
}
// Exception ...
  • 같은 잡을 다수의 코루틴에서 컨텍스트로 사용하는 것이 좀 더 나은 방법
fun main(): Unit = runBlocking {
	val job = SupervisorJob()
	launch(job) {
		delay(1000)
		throw Error("Some error")
	}
	launch(job) {
		delay(2000)
		println("Will be printed")
	}
	job.join()
}
// (1초 후)
// Exception ...
// (1초 후)
// Will be printed

supervisorScope

  • 예외 전파를 막는 또 다른 방법으로 코루틴 빌더를 supervisorScope로 래핑하는 방법이 있음
  • 다른 코루틴에서 발생한 예외를 무시하고 부모와의 연결을 유지함
fun main(): Unit = runBlocking {
	supervisorScope{
		launch(job) {
			delay(1000)
			throw Error("Some error")
		}
	
		launch(job) {
			delay(2000)
			println("Will be printed")
		}
	}
	delay(1000)
	println("Done")
}
// Exception ...
// Will be printed
// (1초 후)
// Done
  • supervisorScope는 중단 함수 본체를 래핑하는데 사용됨
  • 일반적인 사용 방법은 서로 무관한 다수의 작업을 스코프 내에서 실행하는 것
suspend fun notifyAnalytics(actions: List<UserAction>) = 
	supervisorscope {
		actions.forEach { action ->
			launch {
				notifyAnalytics(action)
			}
		}
	}
  • supervisorScopewithContext(SupervisorJob())으로 대체될 수 없음
// 이렇게 하면 안 됩니다!
suspend fun sendNotifications(
	notifications: List<Notification>
) = withContext(SupervisorJob()) {
	for (notification in notifications) {
		launch { 
			client.send(notification)
		}
	}
}

await

suspend fun main() = supervisorScope { 
	val str1 = async<String> {
		delay(1000)
		throw MyException()
	}
	val str2 = async {
		delay(2000)
		"Text2"
	}

	try {
		println(strl.await())
	} catch (e: MyException) { 
		println(e)
	}
	
	println(str2.await())
}
// MyException
// Text2
  • supervisorScope가 사용되어 또 다른 async는 중단되지 않고 끝까지 실행됨

CancellationException은 부모까지 전파되지 않는다 🚫

  • 예외가 CancellationException의 서브클래스라면 부모로 전파되지 않고 현재 코루틴을 취소시킴
object MyNonPropagatingException : CancellationException()

suspend fun main(): Unit = coroutineScope { 
	launch { // 1
		launch { // 2
			delay(2000)
			println("Will not be printed")
		}
		throw MyNonPropagatingException // 3
	}
	
	launch { // 4
		delay(2000)
		print("Will be printed")
	}
}
// (2초 후》
// Will be printed
  1. 두 개의 코루틴이 1과 4의 빌더로 시작됨
  2. 3에서 CancellationException의 서브타입인 MyNonPropagationException 예외를 던짐
  3. 예외는 1에서 시작된 launch에서 잡힘
  4. 1에서 시작된 코루틴은 자기 자신을 취소하고 2에서 정의된 빌더로 만들어진 자식 코루틴을 취소시킴
  5. 4에서 시작된 두 번째 launch는 영향을 받지 않고 2초 후에 “Will be printed”를 출력함

코루틴 예외 핸들러 🤖

  • 예외를 처리하는 기본 행동을 정의하면 유용할 수 있음
  • 이럴 때 CoroutineExceptionHandler 컨텍스트를 사용하면 편리
  • 예외 전파를 중단시키지 않지만 예외가 발생했을 때 해야할 것들을 정의하는 데 사용
fun main(): Unit = runBlocking {
	val handler =
		CoroutineExceptionHandler { ctx, exception ->
			println("Caught sexception")
		}
		
	val scope = CoroutineScope(SupervisorJob() + handler)
	scope.launch {
		delay(1000)
		throw Error("Some error")
	}
	
	scope.launch {
		delay(2000)
		println("Will be printed")
	}
	delay(3000)
}
// Caught java.lang.Error: Some error
// Will be printed

0개의 댓글