티스토리 뷰

728x90

3. 코루틴의 동작 원리 및 구조화된 동시성 (Structured Concurrency)

이제 코틀린 코루틴에서 구조화된 동시성(Structured Concurrency)에 대해 살펴보겠습니다. 이 개념은 코루틴의 안정성과 예외 처리에 중요한 역할을 하며, 코드가 복잡해질수록 필수적으로 이해해야 할 개념입니다.


3.1 코루틴의 동작 원리

3.1.1 중단 함수(suspend 함수)

코루틴은 중단 가능한 함수(suspend 함수)를 통해 비동기 작업을 중단하고 나중에 다시 재개할 수 있습니다. 코루틴 내에서 중단 함수가 호출되면, 해당 코루틴은 중단되며 다른 작업을 계속 진행할 수 있습니다. 코루틴의 핵심은 이러한 비동기 처리를 동기적 코드처럼 작성할 수 있다는 점입니다.

예시로, delay 함수는 중단 함수의 한 예입니다. delay는 코루틴을 일정 시간 동안 중단시키며, 이 시간 동안 다른 코루틴이 자원을 사용할 수 있게 합니다.

3.1.2 코루틴 빌더와 스코프

  • launchasync는 코루틴을 실행하는 대표적인 코루틴 빌더입니다.
  • 각 코루틴은 스코프 내에서 동작하며, 이 스코프가 종료되면 해당 스코프에서 실행되던 모든 코루틴이 자동으로 취소됩니다. 이 스코프가 바로 구조화된 동시성을 이해하는 중요한 개념입니다.

3.2 구조화된 동시성(Structured Concurrency)

구조화된 동시성은 코루틴이 명확한 수명 주기를 가지며, 부모 코루틴과 자식 코루틴 간에 일관된 관계를 가집니다. 이를 통해 코루틴 관리 및 예외 처리가 훨씬 더 명확해집니다.

3.2.1 구조화된 동시성의 기본 개념

  1. 코루틴은 스코프 내에서 동작합니다.
    • 부모 코루틴이 취소되면 그 안에서 실행되는 모든 자식 코루틴이 취소됩니다. 반대로 자식 코루틴이 취소되더라도, 부모 코루틴에 영향을 미치지 않는 구조도 가능합니다.
  2. runBlocking이나 coroutineScope는 구조화된 동시성을 보장하는 중요한 역할을 합니다.
    • runBlocking은 메인 스레드를 블로킹하면서 모든 코루틴이 종료될 때까지 기다립니다.
    • coroutineScope는 메인 스레드를 블로킹하지 않고도, 해당 스코프 내에서 모든 자식 코루틴이 완료될 때까지 기다립니다.

3.2.2 예외 처리와 구조화된 동시성

코루틴의 구조화된 동시성 덕분에 코루틴에서 발생하는 예외는 부모-자식 관계에 따라 안전하게 관리됩니다. 만약 자식 코루틴 중 하나에서 예외가 발생하면, 부모 코루틴은 이를 감지하고 자식 코루틴을 모두 취소하거나 재시작할 수 있습니다.

3.2.3 부모-자식 관계와 구조화된 동시성

부모-자식 코루틴의 관계는 매우 중요합니다. 부모 코루틴은 자식 코루틴을 기다리고, 자식 코루틴에서 예외가 발생하면 그 예외가 부모에게 전파됩니다.

  • launch로 시작된 코루틴은 부모-자식 관계에 의해 예외를 전파합니다.
  • supervisorScope는 자식 코루틴의 예외가 부모에게 전파되지 않도록 하고, 각 자식 코루틴이 서로 독립적으로 실행되게 합니다.

3.3 코루틴 동작 예제: 구조화된 동시성

이제 예제를 통해 구조화된 동시성이 어떻게 동작하는지 알아보겠습니다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Main start: ${Thread.currentThread().name}")

    // 구조화된 동시성으로 자식 코루틴을 관리하는 예제
    coroutineScope {
        launch {
            println("start launch 1")
            delay(1000L)
            println("Task 1 completed on: ${Thread.currentThread().name}")
        }

        launch {
            println("start launch 2")
            delay(500L)
            println("Task 2 completed on: ${Thread.currentThread().name}")
        }

        println("Coroutine scope is running")
    }

    println("Main end: ${Thread.currentThread().name}")
}

코드 설명:

  1. coroutineScope는 자식 코루틴들이 모두 완료될 때까지 기다립니다. 각 자식 코루틴(launch)은 비동기로 실행되지만, 모두 완료되기 전까지 부모 코루틴은 종료되지 않습니다.
  2. coroutineScope는 자식 코루틴들이 모두 완료되면 종료되고, 이후에 "Main end"가 출력됩니다.
  3. 출력되는 순서가 보장되지 않는 이유는 비동기 코루틴이 각기 다른 지연 시간(delay)을 가지고 있기 때문입니다.

출력 결과:

Main start: main
Coroutine scope is running
start launch 1
start launch 2
Task 2 completed on: main
Task 1 completed on: main
Main end: main
  • Task 2는 500ms의 지연 후 먼저 완료되고, Task 1은 1000ms 후 완료됩니다.
  • 마지막에 Main end가 출력되는 것은 모든 자식 코루틴이 종료된 후 부모 코루틴(runBlocking)이 종료되기 때문입니다.

3.4 예외 처리와 구조화된 동시성

이제 예외 처리구조화된 동시성이 어떻게 동작하는지 알아보겠습니다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        coroutineScope {
            launch {
                delay(500L)
                throw Exception("Error in Task 1")  // 예외 발생
            }

            launch {
                delay(1000L)
                println("Task 2 completed on: ${Thread.currentThread().name}")
            }
        }
    } catch (e: Exception) {
        println("Caught an exception: ${e.message}")
    }

    println("Main end: ${Thread.currentThread().name}")
}

코드 설명:

  1. 첫 번째 코루틴(Task 1)에서 예외가 발생하면, 모든 자식 코루틴이 취소됩니다.
  2. coroutineScope 안에서 발생한 예외는 runBlocking부모 스코프로 전파되며, 부모 스코프에서 이를 catch 블록으로 잡아 처리합니다.
  3. 예외가 발생한 후에도, 구조화된 동시성 덕분에 자식 코루틴들은 안전하게 정리됩니다.

출력 결과:

Caught an exception: Error in Task 1
Main end: main
  • Task 1에서 발생한 예외가 부모 코루틴에게 전파되어 "Caught an exception"이 출력됩니다.
  • 자식 코루틴이 예외로 인해 모두 취소되었으므로, Task 2는 실행되지 않고 프로그램이 종료됩니다.

3.5 supervisorScope로 독립적인 예외 처리

supervisorScope를 사용하면, 자식 코루틴 중 하나가 예외를 발생시켜도 다른 자식 코루틴이 영향을 받지 않도록 할 수 있습니다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    supervisorScope {
        launch {
            delay(500L)
            throw Exception("Error in Task 1")  // 예외 발생
        }

        launch {
            delay(1000L)
            println("Task 2 completed on: ${Thread.currentThread().name}")
        }
    }

    println("Main end: ${Thread.currentThread().name}")
}

코드 설명:

  1. supervisorScope 안에서 실행된 자식 코루틴들은 서로 독립적인 예외 처리가 가능합니다. 따라서 하나의 코루틴에서 예외가 발생해도 다른 코루틴에 영향을 미치지 않습니다.
  2. Task 1에서 예외가 발생하지만, Task 2는 계속해서 정상적으로 실행됩니다.

출력 결과:

Task 2 completed on: DefaultDispatcher-worker-1
Main end: main
  • Task 2는 예외와 상관없이 정상적으로 실행되고, 예외는 전파되지 않습니다.

supervisorScope의 예외 처리 방식

  • coroutineScope에서는 자식 코루틴 중 하나에서 발생한 예외부모에게 전파되고, 부모는 그 예외를 감지할 수 있습니다. 따라서 try-catch 블록을 사용하여 예외를 처리할 수 있습니다.
  • 하지만 supervisorScope자식 코루틴들이 서로 독립적으로 실행되며, 한 자식 코루틴의 예외가 다른 자식 코루틴이나 부모 코루틴에 영향을 주지 않습니다. 즉, supervisorScope 내에서 발생한 자식 코루틴의 예외는 부모 코루틴에 전파되지 않으며, 해당 코루틴 내에서 처리되지 않은 예외는 기본적으로 취소되지 않고 종료됩니다. 이는 한 자식 코루틴에서 발생한 예외가 전체 스코프에 영향을 미치지 않도록 설계된 것입니다.

이로 인해, supervisorScope 내에서 예외가 발생해도 부모 코루틴인 runBlocking이 이를 직접 잡을 수 없습니다. 자식 코루틴의 예외는 try-catch 블록으로 잡혀야 합니다.

예외를 잡으려면 어떻게 해야 할까요?

각 자식 코루틴 내에서 예외를 처리해야 합니다. supervisorScope는 예외가 부모에게 전파되지 않으므로, 각 코루틴 안에서 개별적으로 예외 처리를 해야 합니다.

수정된 코드: launch 블록 내에서 예외 처리

import kotlinx.coroutines.*

fun main() = runBlocking {
    supervisorScope {
        launch {
            try {
                delay(500L)
                throw Exception("Error in Task 1")  // 예외 발생
            } catch (e: Exception) {
                println("Caught an exception in Task 1: ${e.message}")
            }
        }

        launch {
            delay(1000L)
            println("Task 2 completed on: ${Thread.currentThread().name}")
        }
    }

    println("Main end: ${Thread.currentThread().name}")
}

코드 설명:

  1. 첫 번째 launch 블록 안에서 try-catch를 사용하여 예외를 처리합니다. supervisorScope는 자식 코루틴의 예외가 부모 코루틴으로 전파되지 않으므로, 예외 처리는 자식 코루틴 내에서 직접적으로 이루어져야 합니다.
  2. 두 번째 launch는 정상적으로 실행되어 "Task 2 completed"를 출력합니다.
  3. 모든 코루틴이 종료되면, Main end가 출력됩니다.

출력 결과:

Caught an exception in Task 1: Error in Task 1
Task 2 completed on: main
Main end: main
  • 첫 번째 코루틴에서 예외가 발생하지만, 해당 예외는 try-catch 블록으로 잡혀서 프로그램이 종료되지 않습니다.
  • 두 번째 코루틴은 정상적으로 실행되며, 그 후 Main end가 출력됩니다.

정리

  • supervisorScope자식 코루틴들 간의 독립성을 보장하며, 한 자식 코루틴에서 발생한 예외가 다른 자식 코루틴이나 부모 코루틴으로 전파되지 않습니다.
  • 예외는 각 코루틴 내에서 직접 처리해야 하며, 자식 코루틴의 예외를 부모 스코프(runBlocking 등)에서 잡으려면 supervisorScope 대신 coroutineScope를 사용해야 합니다.

이렇게 해서 supervisorScope에서의 예외 처리에 대해 더 명확하게 이해할 수 있기를 바랍니다. 추가 질문이 있으면 언제든지 알려주세요!


3.6 구조화된 동시성의 장점

  1. 코루틴의 수명 주기 관리:
    모든 코루틴은 특정 스코프 안에서 관리되므로, 스코

프가 종료될 때 자동으로 코루틴이 종료되거나 정리됩니다. 이를 통해 메모리 누수와 같은 문제가 발생하지 않습니다.

  1. 안전한 예외 처리:
    부모-자식 관계에서 예외가 전파되므로, 한 곳에서 발생한 예외를 부모 스코프에서 처리할 수 있습니다. 또한, supervisorScope를 통해 독립적인 예외 처리가 가능합니다.
  2. 가독성 및 유지보수성 향상:
    비동기 작업을 구조화된 방식으로 작성하면 코드의 가독성과 유지보수성이 크게 향상됩니다. 모든 비동기 작업이 구조화된 방식으로 진행되므로, 흐름을 파악하기 쉽습니다.

요약

  1. 구조화된 동시성(Structured Concurrency)는 코루틴의 수명과 예외 처리를 체계적으로 관리하는 중요한 개념입니다.
  2. 코루틴의 부모-자식 관계 덕분에 예외는 상위 스코프로 전파되며, 예외 처리가 일관되게 이루어집니다.
  3. supervisorScope는 자식 코루틴의 예외가 부모나 다른 자식 코루틴에 영향을 미치지 않도록 할 수 있습니다

 

 

728x90
댓글