ABOUT ME

주로 개발 관련해 이것저것 작성중입니다.

Today
Yesterday
Total
  • CoroutineScope을 정의하는 방법
    개발/Spring(boot) 2025. 3. 10. 00:45
    suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

    이전 글에서 CoroutineScope를 선언하고 Flow와 함께 사용해 SSE를 구현했었고,

    잘 모르는 상태로 아래와 같이 CoroutineScope코드를 정의했었다.

    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    그럼 이번 글에서는 CoroutineScope에 대해 작성해보고자 한다.

    참고로 Docs를 보고 알아볼 예정이지만 백엔드 개발자이기에 안드로이드 관련 내용은 작성하지 않을 예정이다.

    아래와 같은 순서로 작성하겠다.

    1. CoroutineScope?

    1-1. 구조적 동시성(structured concurrency) ?

    2. Context?

    3. CoroutineContext.Element


    1. CoroutineScope?

    interface CoroutineScope

    CoroutineScope는 인터페이스이고 새로운 코루틴의 범위를 정의한다.

    모든 코루틴 빌더(launch, async 등)는 CoroutineScope의 확장 함수로 제공되며, 해당 스코프의 coroutineContext를 상속받아 모든 구성 요소와 취소 정보를 자동으로 전파한다고 한다.

     

    구조적 동시성을 위한 규약(Convention for structured concurrency)
    이 인터페이스를 수동으로 구현하는 것은 권장되지 않으며, 대신 위임(delegation)을 통한 구현을 사용하는 것이 바람직하다. 규약상, 스코프의 컨텍스트는 취소 전파를 통한 구조적 동시성 원칙을 강제하기 위해 Job 인스턴스를 포함해야 한다.

     

    모든 코루틴 빌더(launch, async 등)와 모든 스코핑 함수(coroutineScope, withContext 등)는 내부 코드 블록에 고유의 Job 인스턴스를 가진 자신만의 스코프를 제공한다. 규약에 따라, 이들은 내부의 모든 코루틴이 완료될 때까지 기다린 후에 자신이 완료되어, 구조적 동시성이 보장되도록 한다고 한다.

     

    실제로 CoroutineScope를 사용하려면 아래와 같이 사용할 수 있다.

    fun CoroutineScope(context: CoroutineContext): CoroutineScope

     

    근데 공식 문서를 보다 보니 소문자 coroutineScope도 작성돼 있었다.

    suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

    CoroutineScope는 지정한 컨텍스트로 새로운 스코프 객체를 생성하는 일반 함수이고

    coroutineScope는 suspend 함수로서, 새로운 스코프 내에서 코드를 실행하고 내부에 실행된 모든 코루틴이 완료될 때까지 기다리고 이를 통해 구조적 동시성을 보장한다고 한다.

     

    일반적으로 코루틴을 사용한다고 하면 coroutineScope를 떠올리는게 맞는 것 같고 아래와 같이 사용한다.

    suspend fun showSomeData() = coroutineScope {
        ...
       
        withContext(Dispatchers.Main) {
            doSomeWork()
            val result = data.await()
            display(result)
        }
    }

    추가적으로 구조적 동시성이라는 말이 자꾸 나오기에 한번 찾아보고 넘어가고자 한다.

     


    1-1. 구조적 동시성(structured concurrency)

    구조적 동시성은 여러 동시 실행 작업(예: 코루틴, 쓰레드 등)을 하나의 명확한 범위(스코프) 내에 묶어 관리하는 프로그래밍 패러다임이다.

    이를 통해 개별 작업들의 생명주기, 오류 전파, 취소, 자원 해제 등을 하나의 단위로 통합하여 처리할 수 있다.

     

    구조적 동시성은 다음과 같은 이점을 제공한다.

    • 명확한 범위와 생명주기 관리
      동시 작업들이 명시적으로 정의된 스코프 내에서 실행되고, 그 스코프의 종료와 함께 모든 작업이 완료되거나 취소되므로, 의도치 않은 백그라운드 작업이나 리소스 누수를 방지할 수 있다.
    • 일관된 오류 전파 및 취소
      부모 스코프에서 시작된 작업들 간에는 예외나 취소가 명확하게 전파되며, 한 작업에서 문제가 발생하면 전체 단위가 적절히 취소되어 오류 처리가 쉬워진다.
    • 코드 가독성 및 유지보수성 향상
      동시성 작업의 구조가 코드 블록 단위로 명확하게 드러나므로, 복잡한 비동기 로직을 단순화하고 이해하기 쉽게 만들어 준다.

    구조적 동시성은 동시성 작업들을 하나의 “작업 단위”로 묶어, 오류 처리와 자원 관리, 그리고 전체 프로그램 흐름을 보다 안전하고 예측 가능하게 만들어 준다.

    예를 들어, Kotlin의 coroutineScope { ... }는 내부의 모든 코루틴이 완료될 때까지 기다려 구조적 동시성을 보장한다. 이러한 원칙 덕분에 코드가 더 안전하고 유지보수하기 쉬워진다고 한다.

     

    그럼 이제 CoroutineScope이 인자로 받는 CoroutineContext에 대해 알아보자.


    2. CoroutineContext?

    CoroutineContext 역시 인터페이스다.

    interface CoroutineContext

    코루틴을 위한 지속적인 컨텍스트(Persistent context for the coroutine)

    이 컨텍스트 요소들은 코루틴의 실행 방식, 스케줄링, 라이프사이클, 에러 처리 등을 결정한다.

    Element 인스턴스들의 인덱스가 있는 집합으로, 집합과 맵의 혼합 형태이며 이 집합의 모든 요소는 고유한 Key를 가진다고 한다.

     

    그렇다면 CoroutineContext의 Element에는 어떤 것이 있을까?

    Job, CoroutineDispatcher, CoroutineExceptionHandler, CoroutineName 등이 있다고 한다.

    interface Job : CoroutineContext.Element
    abstract class CoroutineDispatcher : AbstractCoroutineContextElement, ContinuationInterceptor
    interface CoroutineExceptionHandler : CoroutineContext.Element
    data class CoroutineName(val name: String) : AbstractCoroutineContextElement
    
    abstract class AbstractCoroutineContextElement(val key: CoroutineContext.Key<*>) : CoroutineContext.Element

    AbstractCoroutineContextElement역시 CoroutineContext.Element를 상속받으므로 동일하게 보면 된다.

     

    그렇다면 CoroutineContext의 Element에 대해 좀 더 자세히 알아보자.


    3. CoroutineContext.Element

    Element마다 공식 문서 링크를 걸어두었다.

    3-1. Job

    코루틴의 라이프사이클과 취소를 관리하며, 구조적 동시성을 지원

    코루틴의 라이프사이클과 취소(cancellation)를 관리.

     

    - Job

     

    • 라이프사이클 관리
      Job은 코루틴이 active, completing, completed, cancelled 등의 상태를 가지며, 이 상태들을 통해 현재 실행 중인 코루틴의 진행 상황을 추적 가능.
    • 취소(Cancellation)
      cancel() 메서드를 호출하면 코루틴이 취소되고, 이때 CancellationException이 발생하여 코루틴의 실행 중단. 이를 통해 불필요한 작업을 중지하고 자원 회수 가능.
    • 구조적 동시성(Structured Concurrency)
      코루틴은 계층적인 Job 구조를 형성. 부모 Job은 자식 Job들을 포함하며, 부모가 취소되면 모든 자식 코루틴도 함께 취소. 이는 복잡한 비동기 작업에서 오류 전파 및 자원 관리를 용이하게 함.
    • 조인(Join) 메커니즘
      join() 메서드를 사용하면 특정 Job이 완료될 때까지 기다릴 수 있음. 이는 여러 코루틴의 종료 시점을 동기화할 때 유용.

     

    - SupervisorJob

     

    • 기본적으로 Job과 동일한 기능 제공
    • 자식 코루틴 간의 독립성 보장
    • 한 자식 코루틴에서 예외가 발생해도, 다른 자식 코루틴들은 계속 정상적으로 실행.
    • 부모 코루틴은 자식의 실패를 인지하긴 하지만, 전체 스코프에 전역 취소를 적용하지 않고, 개별적으로 처리.

     

     

     

     


    3-2. CoroutineDispatcher

    코루틴이 실행될 스레드 또는 스레드 풀 지정

    - Dispatchers.Default: CPU 집약적인 작업에 적합.

    - Dispatchers.IO: I/O 작업(파일, 네트워크 등)에 최적화.

    - Dispatchers.Main: UI 스레드에서 실행할 필요가 있는 작업에서 사용.

     

    참고로 궁금해서 Dispatcher에 대해 찾아봤더니 셋 모두 CoroutineDispatcher를 반환하는 것을 확인했다.

    expect val Default: CoroutineDispatcher
    val IO: CoroutineDispatcher
    expect val Main: MainCoroutineDispatcher
    
    abstract class MainCoroutineDispatcher : CoroutineDispatcher

     

     


    3-3. CoroutineExceptionHandler

    코루틴에서 처리되지 않은 예외가 발생하면 이를 가로채어 처리.

    기본적으로 코루틴은 예외를 상위 컨텍스트로 전파하지만, 이 핸들러를 추가하면 로그를 남기거나, 특정 동작을 수행하는 등 커스텀 예외 처리 가능.

     


    3-4. CoroutineName

    코루틴에 이름을 부여하여 디버깅이나 로깅 시 추적을 용이하도록 함.

    여러 코루틴을 실행할 때 각 코루틴의 이름을 지정함으로써, 로그나 스택 트레이스에서 특정 코루틴을 식별 가능.

     

     


    모든 요소를 결합해 아래와 같이 CoroutineScope를 생성할 수 있다.

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        
        // Custom Exception Handler
        val exceptionHandler = CoroutineExceptionHandler { _, exception ->
            println("코루틴 예외 발생: $exception")
        }
        
        // SupervisorJob, Dispatchers, CoroutineName, CoroutineExceptionHandler 결합
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineName("MyScope") + exceptionHandler)
        
        // scope 내부에서 코루틴 실행
        scope.launch {
            println("코루틴 실행: ${coroutineContext[CoroutineName]?.name}")
            // 작업 수행...
        }
        
        // scope 내의 모든 작업이 완료될 때까지 대기
        scope.coroutineContext[Job]?.join()
    }

    CoroutineScope에 대해 작성해보았다.

    Kotlin Docs를 읽어보며 작성해봤는데 interface가 많아서 그런지 구현체에 대한 링크가 없어 아쉬웠다.

     

    그래도 궁금했던 CoroutineScope와 Job에 대해 알게 되었고, 추가적으로 CoroutineContext에 대해 자세히 알아볼 수 있어 좋았다.

     

     

     

    출처

    https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/

    https://openjdk.org/jeps/453

     

    반응형

    댓글

Designed by Tistory.