2024. 5. 16. 15:07ㆍWeb/Spring
1. 코루틴이 왜 필요할까?
코루틴은 비동기 프로그래밍을 단순화하고, 효율적인 동시성(concurrency) 관리를 가능하게 하는 프로그래밍 개념입니다. 코루틴은 경량 스레드(lightweight thread)와 같이 작동하는데, 개발자가 프로그램의 어느 시점에서든 실행을 일시 중지하고 필요할 때 재개할 수 있게 해줍니다. 이러한 특성 덕분에 코루틴은 네트워크 호출, 데이터베이스 트랜잭션과 같은 비동기 작업을 쉽고 효과적으로 처리할 수 있으며, 이런 작업들을 마치 동기적 코드처럼 보이게 만들어 줍니다.
필요성은 비동기 코드를 더 간결하고 이해하기 쉽게 만들며, 자원을 효율적으로 사용할 수 있습니다. 전통적인 멀티 스레딩 접근 방식에 비해 메모리 사용을 줄이고, 컨텍스트 스위칭의 오버헤드를 감소시킬 수 있어 애플리케이션의 성능을 향상시킬 수 있습니다.
1. 경량 스레드
코루틴은 스레드보다 훨씬 적은 메모리를 사용합니다. 전통적인 스레드는 각각 독립된 메모리 스택을 소유하고, 이는 대략 1MB 정도의 메모리를 차지할 수 있습니다. 반면, 코루틴은 이러한 스레드 스택을 공유하고, 필요한 경우에만 작은 스택 메모리를 할당하여 사용하므로, 수천 개의 코루틴이 동시에 실행되어도 스레드를 사용했을 때보다 훨씬 적은 메모리를 사용합니다.
2. 컨텍스트 스위칭 감소
컨텍스트 스위칭은 CPU가 한 작업에서 다른 작업으로 전환할 때 발생하는 프로세스로, 스레드 간의 컨텍스트 스위칭은 상당한 오버헤드를 유발할 수 있습니다. 코루틴을 사용하면, 스레드 수보다 훨씬 많은 코루틴을 동시에 관리할 수 있으며, 코루틴은 사용자가 정의한 지점에서만 스케줄링을 허용하기 때문에 필요할 때만 컨텍스트 스위칭이 발생합니다. 이렇게 컨트롤되고 예측 가능한 스위칭은 시스템 자원의 효율적 사용을 가능하게 하며, 전체적인 성능을 향상시킵니다.
(쓰레드가 바뀌어도 실행 정보를 코루틴이 모두 가지고 있기 때문에.)
3. 더 효율적인 비동기 프로그래밍
코틀린 코루틴은 비동기 프로그래밍을 보다 쉽게 만들어, 비동기 함수를 동기적인 방식으로 작성할 수 있게 합니다. 이는 코드의 복잡성과 버그가 발생할 확률을 줄여줍니다. 비동기 작업에서 코루틴은 작업이 완료될 때까지 스레드를 차단하지 않고, 대신 코드의 특정 부분을 일시 중단하여 리소스가 필요할 때만 사용합니다. 이는 리소스 사용과 전력 소비를 줄이고, 애플리케이션의 반응성을 향상시킵니다.
코틀린에서 코루틴의 특별한 점
ㄴ코틀린은 코루틴을 언어 수준에서 지원합니다. 코틀린의 코루틴은 몇 가지 특별한 점을 가지고 있습니다
1. 간결성: 코틀린은 suspend 키워드를 사용하여 비동기 함수를 쉽게 선언할 수 있게 지원하며, 이는 코드를 간결하게 유지하면서도 강력한 비동기 처리를 가능하게 합니다.
2. 구조화된 동시성: 코틀린의 코루틴은 스코프를 기반으로 동작하므로, 코루틴의 생명주기가 그 스코프에 종속됩니다. 이는 리소스를 안전하게 관리하도록 하며, 메모리 누수나 다른 문제점을 방지하는 데 도움을 줍니다.
3. 콘텍스트 관리: 코틀린 코루틴은 실행 콘텍스트(예: 디스패처)를 명시적으로 지정할 수 있어, 개발자가 세밀하게 작업을 제어할 수 있습니다. 이는 UI 작업이나 백그라운드 작업을 적절히 처리할 때 특히 유용합니다.
4. 쉬운 통합과 확장: 코틀린 코루틴은 리액티브 프로그래밍 모델과도 잘 통합되며, 기존의 콜백 기반 라이브러리나 프레임워크와도 쉽게 결합할 수 있어, 기존 코드베이스로의 증분 도입이 용이합니다. 코틀린 코루틴을 배우는 것은 현대의 비동기 및 동시성 프로그래밍 요구에 부응할 수 있는 현명한 선택입니다. 개발자로서 코드의 유지보수성을 높이고, 성능을 개선하며, 다양한 환경과 상황에서 더 나은 사용자 경험을 제공하기 위해 코루틴을 습득하는 것이 중요합니다.
2. 중단(suspend)이란 무엇이며 어떻게 작동할까?
중단(suspend)이란 무엇인가?
중단이란 코틀린의 코루틴에서 사용되는 키워드로, 실행을 일시 중지할 수 있게 해주는 프로그래밍 메커니즘입니다. suspend라는 키워드는 특정 함수 앞에 위치하여 해당 함수가 비동기적으로 동작하며, 필요한 경우 실행을 멈추고 나중에 다시 시작할 수 있음을 의미합니다. 이러한 중단 가능 함수는 리소스가 바쁘다거나 응답을 기다리는 등의 이유로 즉시 완료할 수 없는 작업에 유용합니다.
중단 함수의 작동 원리
중단 함수는 코루틴의 실행을 ‘일시 중지’할 수 있는 지점을 제공합니다. 이 함수들은 suspend 키워드를 통해 정의되며, 코루틴 스케줄러의 관리 하에 실행됩니다. 중단 함수 내에서, 함수는 백그라운드 작업(예: 네트워크 요청, 긴 계산 등)이 완료될 때까지 코루틴의 실행을 중지할 수 있고, 그 실행은 호출 스택을 차단하지 않으면서도 나중에 자동으로 재개됩니다. 중단 지점에서 코드의 실행이 멈춘 후, 코루틴은 백그라운드 작업의 완료를 기다리는 동안 다른 작업을 처리할 수 있는 자원(스레드 등)을 해제합니다. 코틀린 컴파일러는 중단 함수를 처리할 때 내부적으로 상태 머신을 구축합니다. 이 상태 머신은 함수의 중단과 재개 지점을 관리합니다. 함수가 중단되면, 상태 머신은 해당 시점과 변수의 상태를 저장하고, 작업이 완료되면 저장된 상태에서 다시 작업을 재개할 수 있도록 합니다.
코루틴과 스레드의 상호 작용
코루틴은 스레드와는 독립적으로 작동하지만, 실행을 위해서는 스레드가 필요합니다. 코틀린의 코루틴은 기본적으로 스레드 위에서 실행되지만, 스레드와 달리 적은 비용으로 생성 및 관리가 가능하며, 수천 개의 코루틴이 단일 또는 소수의 스레드에서 실행될 수 있습니다. 코루틴은 스레드를 차지하는 것이 아니라, 필요할 때만 스레드의 자원을 사용하여 실행되고, 필요 없을 때는 자원을 반환합니다. 이는 스레드의 효율적 사용을 가능하게 하며, 애플리케이션의 성능 향상에 기여합니다.
요약하자면, 중단 함수와 코루틴의 상호 작용은 효율적인 동시성 관리와 비동기 태스크의 간소화를 가능하게 하며, 이는 현대 소프트웨어 개발에서 중요한 요소입니다. 이러한 기능을 통해 코틀린은 개발자가 보다 쉽고 효과적으로 비동기 로직을 구현할 수 있게 해 줍니다.
그래서 중단 시키는 게 뭔데?
즉 코루틴 빌더(launch, async 등)을 이용해 suspend 함수를 사용하면 결국은 새로운 코루틴이 생기는 것인데, 그때 그 함수를 호출한 호출자는 잠시 실행을 멈춘다. (이 또한 다른 코루틴이고 다른 스레드에서 실행되고 있을 테니까) 실행을 멈추게 되면 사용하고 있던 스레드는 반납되고 호출된 새로운 코루틴이 작업을 시작한다.
일시정지 될 때는 Continuation 객체를 반환하는데, Continuation 객체는 멈췄던 곳에서 다시 코루틴을 실행할 수 있다.(다른 Thread에서 실행 될 수도 있음) 여기서 Thread와 코루틴의 차이라면, Thread는 저장이 불가능하고, 멈추는 것만 가능하다. 하지만 Continuation은 멈추는 것은 물론 이전의 실행 상태를 저장하고 있다.(컨텍스트 스위칭 비용이 줄어드는 것이 여기서 나오는 것이다.)
3. 코루틴의 실제 구현
1. suspend 함수는 함수가 시작할 때와 suspend 함수가 호출되었을 때 상태를 가진다는 점에서 상태 머신(state machine)과 비슷하다.
2. Continuation 객체는 상태를 나타내는 숫자와 로컬 데이터를 가지고 있다.
3. 함수의 Continuation 객체가 이 함수를 부르는 다른 함수의 Continuation 객체를 decorate한다. 그 결과, 모든 Continuation 객체는 실행을 재개하거나 재개된 함수를 완료할 때 사용되는 콜 스택으로 사용된다.
@Test
fun main() = runTest {
println("Before")
// 코루틴 실행을 멈춘다.
val value = suspendCoroutine<String> { continuation ->
Thread.sleep(1000)
// 리턴할 결과를 만들었으면 자신을 호출한 컨티뉴에이션에게 결과값과 함께 재개(resume)한다.
continuation.resumeWith(Result.success("SuspendFunction"))
}
println(value)
println("After")
}
//Before
//(1초 후)
//SuspendFunction
//After
위 코드를 보고 설명하자면, suspendCoroutine으로 시작하는 코드 라인에서 이 함수를 호출한 main()함수의 실행은 중지된다. 이때 main함수를 실행 중이던 쓰레드는 계속 기다리지 않고 다른 일을 하러 가고, suspendCoroutine 실행이 모두 완료되면 호출한 코루틴의 Continuation에게 결과 값을 넘기고 resume한다. 그러면 멈춘 지점부터 실행을 재개하게 된다.
위 코드는 suspend 함수 예제로 실제 유사 구현을 보자면 아래와 같다.
// 우리가 작성한 함수
suspend fun myFunction() {
println("Before")
// suspend function
delay(1000)
println("After")
}
// 컴파일 후 작성된 코드
fun myFunction(continuation: Continuation<Unit>): Any {
// 호출자 코루틴의 Continuation을 decorate
val continuation = continuation as? MyContinuation?: MyContinuation(continuation)
// suspend function이 등장하는 시점마다 label을 하나씩 증가시킨다.
if (continuation.label == 0) {
println("Before")
continuation.label = 1
if (delay(1000, continuation) == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
}
// label 값을 통해 중지 후 재실행되는 지점을 관리한다.
if (continuation.label == 1) {
println("After")
return Unit
}
error("Impossible")
}
class MyContinuation(
val completion: Continuation<Unit>
): Continuation<Unit> {
override val context: CoroutineContext
get() = completion.context
var label = 0
var result: Result<Any>? = null
override fun resumeWith(result: Result<Unit>) {
this.result = result
var res = try {
val r = myFunction(this)
if (r == COROUTINE_SUSPENDED) return
Result.success(r as Unit)
} catch (e: Throwable) {
Result.failure(e)
}
completion.resumeWith(res)
}
}
결론적으로 위 코드를 보면 코루틴마다 Continuation 객체를 갖게되는데, 그 Continuation 객체에는 일시정지된 위치값(label) 그리고 다른 코루틴의 결과 값(local variable) 등을 가져서 일시 중지된 부분부터 재실행이 가능하다. 즉, 일단 쓰레드 모델처럼 콜스택에 실행 정보를 가지고 있어서 매번 컨텍스트 스위칭을 하는 것이 아니라 코루틴 자체적으로 실행정보를 가지기에 불필요한 쓰레드간 컨텍스트 스위칭이 발생하지 않는다.
(작성중..)
'Web > Spring' 카테고리의 다른 글
Spring kafka + micrometer tracing (0) | 2024.05.14 |
---|---|
Micrometer Tracing (0) | 2024.04.30 |
Spring kafka 설명 (0) | 2023.11.05 |
Spring Kafka commit 정책 (0) | 2023.11.05 |
대용량 트래픽관련 유튜브 영상들 (0) | 2023.11.02 |