Ecosystem

Kotlin Coroutines 1.5: 섬세하고 개선된 채널 API로 표시되는 GlobalScope을 비롯한 많은 개선 사항

Read this post in other languages:

공동 작성자: Svetlana Isakova

Kotlin Coroutines 1.5.0이 출시되었습니다! 새 버전에 포함된 기능을 아래에서 살펴보세요.

  • 개선된 채널 API. 라이브러리 함수에 대한 새로운 명명 체계와 함께 offerpoll에 대한 더 나은 대안으로 비중단 함수 trySendtryReceive가 도입되었습니다.

이 블로그 게시물에서는 새 버전으로 마이그레이션하기 위한 권장 사항도 소개합니다.

Coroutines 1.5.0 사용 시작하기

섬세한 API로 표시되는 GlobalScope

GlobalScope 클래스는 이제 @DelicateCoroutinesApi 어노테이션으로 표시됩니다. 이제부터 GlobalScope를 사용하려면 @OptIn(DelicateCoroutinesApi::class)를 이용한 명시적 옵트인이 필요합니다.

GlobalScope의 사용은 대부분의 경우 권장되지 않지만 공식 문서는 여전히 이 섬세한 API를 통해 개념을 소개합니다.

전역 CoroutineScope는 잡(job)에 바인딩되지 않습니다. 전역 범위는 전체 애플리케이션 수명 기간 동안 작동하고 조기에 취소되지 않는 최상위 코루틴을 시작하는 데 사용됩니다. GlobalScope에서 시작된 활성화된 코루틴은 프로세스 중지를 막지 않습니다. 데몬 스레드와 비슷합니다.

이것은 섬세한 API이며 GlobalScope를 사용할 때 실수로 리소스 또는 메모리 누수를 발생시키기 쉽습니다. GlobalScope에서 시작된 코루틴은 구조화된 동시성 원칙의 적용을 받지 않으므로 문제(예: 느린 네트워크로 인해)가 발생하여 중단되거나 지연되는 경우, 작동을 멈추지 않고 리소스를 소비합니다. 예를 들어, 다음 코드를 생각해 보겠습니다.

loadConfiguration을 호출하면 GlobalScope에 코루틴이 생성되어 취소하거나 완료될 때까지 대기하기 위한 어떤 프로비전도 없이 백그라운드에서 작동합니다. 네트워크가 느리면 코루틴이 백그라운드에서 계속 대기하면서 리소스를 소비합니다. loadConfiguration에 반복적으로 호출이 이루어지면 점점 더 많은 리소스가 소비됩니다.

가능한 대체

대부분의 경우 GlobalScope 사용을 피해야 하며 GlobalScope를 포함하는 작업은 <0>suspend로 표시되어야 합니다. 예를 들면 다음과 같습니다.

GlobalScope.launch를 사용하여 여러 작업을 동시에 시작하는 경우, 해당 작업을 대신 coroutineScope로 그룹화해야 합니다.

최상위 코드에서 비중단 컨텍스트로 동시 작업을 시작할 때는 GlobalScope 대신 적절하게 제한된 CoroutineScope 인스턴스를 사용해야 합니다.

적합한 사용 사례

GlobalScope를 적합하고 안전하게 사용할 수 있는 제한된 상황이 있습니다. 예를 들면, 애플리케이션의 전체 수명 기간 동안 활성 상태로 유지되어야 하는 최상위 백그라운드 프로세스가 그렇습니다. 따라서 GlobalScope를 사용하려면 다음과 같이 @OptIn(DelicateCoroutinesApi::class)를 사용한 명시적 옵트인이 필요합니다.

GlobalScope의 모든 사용을 주의 깊게 검토하고 “적합한 사용 사례” 범주에 해당하는 경우에만 어노테이션을 추가하는 것이 좋습니다. 다른 용도로 사용하면 코드에 버그가 생기는 원인이 될 수 있습니다. 위의 설명에 따라 GlobalScope 사용을 바꾸는 것이 좋습니다.

JUnit 5 확장 기능

별도의 스레드에서 테스트를 실행하여 제공된 시간 제한 후에 실패하고 스레드를 중단할 수 있는 CoroutinesTimeout 어노테이션을 추가했습니다. 이전에는 JUnit 4에서 CoroutinesTimeout을 사용할 수 있었습니다. 이 릴리스에서는 JUnit 5에 대한 통합을 추가했습니다.

새 어노테이션을 사용하려면 프로젝트에 다음 종속성을 추가하세요.

다음은 테스트에서 새로운 CoroutinesTimeout을 사용하는 방법에 대한 간단한 예입니다.

이 예에서 코루틴 시간 제한은 클래스 수준에서, 특히 firstTest에 대해 정의됩니다. 함수의 어노테이션이 클래스 수준 어노테이션을 재정의하므로 어노테이션이 추가된 테스트는 시간이 초과되지 않습니다. secondTest는 클래스 수준 어노테이션을 사용하므로 시간이 초과됩니다.

어노테이션은 다음과 같은 방식으로 선언됩니다.

첫 번째 매개변수인 testTimeoutMs는 시간 초과 기간을 밀리초 단위로 지정합니다. 두 번째 매개변수인 cancelOnTimeout은 시간이 초과되는 시점에 실행 중인 모든 코루틴을 취소해야 하는지 여부를 결정합니다. true로 설정하면 모든 코루틴이 자동으로 취소됩니다.

CoroutinesTimeout 어노테이션을 사용할 때마다 자동으로 코루틴 디버거를 실행하고 시간 초과 시점에 모든 코루틴을 덤프합니다. 덤프에는 코루틴 생성 스택 추적이 포함됩니다. 테스트 속도를 높이기 위해 생성 스택 추적을 비활성화해야 하는 경우, 이 구성을 가능하게 하는 CoroutinesTimeoutExtension를 직접 사용하는 것이 좋습니다.

JUnit 5용 CoroutinesTimeout을 위해 유용한 PoC를 만들어준 Abhijit Sarkar에게 감사드립니다. 이 아이디어는 1.5 릴리스에 추가된 새로운 CoroutinesTimeout 어노테이션으로 구현되었습니다.

채널 API 개선

채널은 서로 다른 코루틴과 콜백 사이에 데이터를 전달할 수 있는 중요한 통신 기본 요소입니다. 이 릴리스에서는 혼동을 일으키는 offerpoll 함수를 더 나은 대안으로 대체하여 채널 API를 조금 손 보았습니다. 그 과정에서 중단 및 비중단 메서드에 대해 일관된 명명 체계를 새롭게 개발했습니다.

새로운 명명 체계

다른 라이브러리 또는 Coroutines API에서 더 많이 사용하도록 하기 위해 일관된 명명 체계를 수립하는 데 노력을 기울였습니다. 함수의 이름이 해당 동작에 대한 정보를 전달할 수 있도록 하는 데 신경을 썼습니다. 그래서 다음과 같은 결과를 얻었습니다.

  • 일반적인 중단 메서드는 그대로 유지됩니다(예: send, receive).
  • 오류 캡슐화를 사용하는 비중단 메서드 앞에는 계속해서 예전의 offerpoll 대신에 “try”: trySendtryReceive가 붙습니다.
  • 새로운 오류 캡슐화 중단 메서드 끝에는 “Catching”이 붙습니다.

이러한 새로운 메서드에 대해 자세히 살펴보겠습니다.

Try 함수: sendreceive에 대한 비 일시 중단 대응 항목

어떤 코루틴은 일부 정보를 채널로 보낼 수 있고, 또 다른 코루틴은 여기서 정보를 받을 수 있습니다. sendreceive 함수 모두가 일시 중단됩니다. send는 채널이 가득 차서 새 요소를 받을 수 없는 경우 코루틴을 일시 중단하고, receive는 채널에 반환할 요소가 없는 경우 코루틴을 일시 중단합니다.

이러한 함수에는 동기 코드에서 사용하기 위한 비중단 대응 함수인 offerpoll이 있습니다. 이러한 함수는 곧 사용할 수 없게 되고 현재는 trySendtryReceive가 대신 사용됩니다. 이렇게 변경한 이유에 대해 알아보겠습니다.

offerpollsendreceive와 동일한 작업을 수행하도록 의도되었지만 일시 중단이 없습니다. 이것이 이해하기 쉽고, 요소를 보내거나 받을 수 있을 때 모든 것이 잘 작동합니다. 하지만 오류가 발생하면 어떻게 될까요? sendreceive는 작업을 수행할 수 있을 때까지 일시 중단합니다. offerpoll은 채널이 가득 차서 요소를 추가할 수 없는 경우, 또는 채널이 비어 있어 요소를 가져올 수 없는 경우에 각각 falsenull을 반환하기만 했습니다. 둘 모두 닫힌 채널로 작업하려고 예외를 던졌으며, 이 마지막 동작이 혼란을 일으켰습니다.

이 예에서 poll은 요소가 추가되기 전에 호출되므로 즉시 null을 반환합니다. 이러한 사용 방식은 원래 의도한 것이 아닌 점을 유의해 주세요. 대신, 요소를 계속해서 정기적으로 폴링해야 하지만 이 설명에서는 이해를 돕기 위해 직접 호출하겠습니다. 또한, 우리가 사용하는 채널은 랑데뷰 채널이고 버퍼 용량이 없기 때문에 offer 호출이 실패합니다. 결과적으로, 순전히 잘못된 순서로 호출되었다는 이유로 offerfalse를 반환하고 pollnull을 반환합니다.

위의 예에서 channel.close() 문의 주석 처리를 제거하여 예외가 던져지는지 확인해 보세요. 이 경우 poll은 이전과 같이 false를 반환합니다. 그러나 offer는 이미 닫힌 채널에 요소를 추가하려고 하지만 실패하고 예외를 던집니다. 저희는 이러한 동작에 오류가 많다는 불만을 많이 받았습니다. 이 예외는 간과하기 쉬우며 무시하거나 다르게 처리하면 프로그램이 충돌합니다.

새로운 trySendtryReceive는 이 문제를 해결하고 더 상세한 결과를 반환합니다. 각각이 ChannelResult 인스턴스를 반환하는데, 이는 성공한 결과, 실패 또는 채널이 닫혔음을 나타내는 세 가지 중 하나입니다.

이 예제는 tryReceivetrySend가 더 상세한 결과를 반환한다는 점만 제외하면 이전 예제와 동일한 방식으로 작동합니다. 출력에서 falsenull 대신 Value(Failed)를 볼 수 있습니다. 채널을 닫는 줄의 주석 처리를 다시 제거하고 trySend가 이제 예외를 캡처하는 Closed 결과를 ​​반환하는지 확인합니다.

inline value 클래스 덕분에 ChannelResult를 사용해도 아래에 추가 래퍼가 생성되지 않으며, 값이 성공적으로 반환되면 오버헤드 없이 있는 그대로 반환됩니다.

Catching 함수: 오류를 캡슐화하는 일시 중단 함수

이번 릴리스부터 오류 캡슐화 일시 중단 메서드 끝에는 “Catching”이 붙습니다. 예를 들어, 새로운 receiveCatching 함수는 닫힌 채널의 경우 예외를 처리합니다. 다음의 간단한 예를 생각해 보겠습니다.

채널은 값을 검색하기 전에 닫힙니다. 그러나, 프로그램이 성공적으로 완료되어 채널이 닫힌 것으로 나타납니다. receiveCatching을 일반 receive 함수로 바꾸면 ClosedReceiveChannelException을 던집니다.

지금은 receiveCatchingonReceiveCatching만 제공하지만(이전의 내부 receiveOrClosed 대신) 더 많은 함수를 추가할 계획입니다.

코드를 새 함수로 마이그레이션

프로젝트에서 offerpoll 함수의 모든 인스턴스를 자동으로 새 호출로 바꿀 수 있습니다. offerBoolean을 반환했기 때문에 이에 상응하는 대체는 channel.trySend("Element").isSuccess입니다.

마찬가지로, poll 함수는 null 가능 요소를 반환하므로 그 대체는 channel.tryReceive().getOrNull()이 됩니다.

호출 결과가 사용되지 않은 경우, 직접 새 호출로 바꿀 수 있습니다.

이제 예외 처리에 대한 동작이 다르므로 필요한 업데이트를 수동으로 해야 합니다. 코드가 닫힌 채널에서 예외를 던지는 ‘offer’ 및 ‘poll’ 메서드에 의존하는 경우, 다음 대체 방법을 사용해야 합니다.

channel.offer("Element")에 대한 동등한 대체 요소는 채널이 정상적으로 닫혔더라도 예외를 던져야 합니다.

channel.poll()의 동등한 대체 요소는 채널이 오류와 함께 닫혔을 경우 예외를 던지고 정상적으로 닫혔다면 null을 반환합니다.

이러한 변경 사항은 offerpoll 함수의 이전 동작을 반영합니다.

대부분의 경우, 코드가 닫힌 채널에서 이러한 미묘한 동작에 따라 좌우된 것이 아니라, 버그의 원인이었다고 가정합니다. 이것이 IDE에서 제공하는 자동 대체 기능이 의미를 단순화하는 이유입니다. 실제로 여기에 해당되지 않는 경우, 사용 위치를 수동으로 검토 및 업데이트하고, 예외를 던지지 않고 닫힌 채널을 다르게 처리하도록 코드를 완전히 다시 작성하는 것이 좋습니다.

안정성으로 가는 Reactive 통합

Kotlin Coroutines 버전 1.5는 반응형 프레임워크와의 통합을 담당하는 대부분의 함수를 안정적인 API로 승격시켰습니다.

JVM 에코시스템에는 Reactive Streams 표준에 따라 비동기 스트림을 처리하는 몇 가지 프레임워크가 있습니다. 예를 들어, Project ReactorRxJava는 이 영역에서 널리 사용되는 두 가지 Java 프레임워크입니다.

Kotlin Flows는 이와 다르고 타입이 표준에 지정된 것과 일치하지는 않지만 개념적으로는 여전히 스트림입니다. Flow를 반응형(사양 및 TCK 준수) Publisher로 변환할 수 있고 그 반대도 마찬가지입니다. 이러한 변환기는 kotlinx.coroutines에서 즉시 제공되며 해당 반응형 모듈에서 찾을 수 있습니다.

예를 들어, Project Reactor 타입과의 상호 운용성이 필요한 경우 프로젝트에 다음 종속성을 추가해야 합니다.

그러면 Reactive Streams 타입을 사용하려는 경우 Flow<T>.asPublisher()를 사용하거나, Project Reactor 타입을 직접 사용해야 하는 경우 Flow<T>.asFlux()를 사용할 수 있습니다.

이것은 관련 주제를 매우 압축적으로 보여주는 뷰입니다. 더 자세히 알아보려면 Reactive Streams 및 Kotlin Flows에 대한 Roman Elizarov의 글을 읽어보세요.

반응형 라이브러리와의 통합이 API 안정화 방향으로 이루어지고 있지만 기술적인 목표는 @ExperimentalCoroutinesApi를 없애고 남아있는 이슈를 수정하는 것입니다.

Reactive Streams와의 통합 개선

타사 프레임워크와 Kotlin 코루틴 간의 상호 운용성을 보장하려면 Reactive Streams 사양과의 호환성이 중요합니다. 그러면 모든 코드를 다시 작성할 필요없이 기존 프로젝트에서 Kotlin 코루틴을 채택하는 데 도움이 됩니다.

이번에 안정적인 상태로 올려 놓은 함수들이 많이 있습니다. 이제 모든 Reactive Streams 구현에서 Flow로, 또는 그 반대로 유형을 변환할 수 있습니다. 예를 들어, 새로운 코드는 코루틴으로 작성될 수 있지만 반대 변환기를 통해 이전 반응 코드베이스와 통합됩니다.

또한 Reactor의 ContextCoroutineContext로 래핑하는 ReactorContext에 수 많은 개선 사항이 적용되어 Project Reactor와 Kotlin 코루틴 사이에서 완벽한 통합을 지원합니다. 이 통합으로 코루틴을 통해 Reactor의 Context에 대한 정보를 전파할 수 있습니다.

컨텍스트는 Mono, Flux, Publisher.asFlow, Flow.asPublisherFlow.asFlux와 같은 모든 Reactive 통합에 의해 subscriber를 통해 묵시적으로 전파됩니다. 다음은 subscriber의 ContextReactorContext에 전파하는 간단한 예입니다.

위의 예에서는 Flow 인스턴스를 생성한 다음, 컨텍스트 없이 이를 Reactor의 Flux 인스턴스로 변환합니다. 인수 없이 subscribe() 메서드를 호출하면 publisher에게 모든 데이터를 보내도록 요청하는 효과가 있습니다. 결과적으로, 프로그램은 “Reactor context in Flow: null“이라는 문구를 출력합니다.

다음 호출 체인도 FlowFlux로 변환하지만, 그 다음에 이 체인에 대한 Reactor의 컨텍스트에 키-값 쌍 answer=42를 추가합니다. subscribe()를 호출하면 체인이 트리거됩니다. 이 경우 컨텍스트가 채워지므로 프로그램은 “Reactor context in Flow: Context1{answer=42}”를 출력합니다.

새로운 편의 함수

코루틴 컨텍스트에서 Mono와 같은 반응형으로 작업할 때 스레드를 차단하지 않고 검색을 허용하는 몇 가지 편의 함수가 있습니다. 이 릴리스에서는 임의의 Publisher에서 awaitSingleOr* 함수를 지원 중단했으며 MonoMaybe에 대해 일부 await* 함수를 특수화했습니다.

Mono는 최대 하나의 값을 생성하므로 마지막 요소는 첫 번째 요소와 동일합니다. 이 경우 나머지 요소를 삭제할 때의 의미도 유용하지 않습니다. 따라서 Mono.awaitFirst()Mono.awaitLast()에 대한 지원이 중단되고 Mono.awaitSingle()이 사용됩니다.

kotlinx.coroutines 1.5.0을 사용해 보세요!

새 릴리스에는 놀라울 정도로 많은 변경 사항이 적용되었습니다. Channels API를 개선하는 동안 개발된 새로운 이름 지정 체계는 주목할 만한 성과입니다. 한편, Coroutines API를 최대한 단순하고 직관적으로 만드는 데 중점을 두고 있습니다.

새 버전의 Kotlin 코루틴 사용을 시작하려면 build.gradle.kts 파일의 콘텐츠를 업데이트하기만 하면 됩니다. 먼저, 최신 버전의 Kotlin Gradle 플러그인이 있는지 확인해 주세요.

그런 다음 Reactive Streams에 대한 특정한 통합이 있는 라이브러리를 포함하여 종속성 버전을 업데이트하세요.

동영상 및 읽을거리

문제가 있는 경우,

이 게시물은 Anton Arhipov가 작성한 Kotlin Coroutines 1.5: GlobalScope Marked as Delicate, Refined Channels API, and More를 번역한 글입니다.

image description

Discover more