코루틴 개념
코루틴은 경량쓰레드이다.
코루틴은 협력적(비선점형) 스케줄링으로 명시적인 컨텍스트 스위칭이 필요하며, 개발자가 컨트롤을 가져간다.
즉, 현재 실행 중인 코루틴이 명시적으로 제어권을 넘겨주기(suspend) 전까지는 다른 코루틴이 실행되지 않는다.
이 비선점형 특성은 코루틴의 동작 방식과 이어진다.
코틀린 코루틴은 컴파일 타임에 상태 머신 클래스으로 변환된다.
- 이는 코루틴이 suspend와 resume을 통해 중단되었다가 이어지는 동작을 한다는 것이다.
- 코루틴이 중단(suspend)되는 순간에만 다른 코루틴이 실행될 수 있다.
- 따라서 비선점형 환경에서는 작업이 적절히 suspend되지 않으면, 다른 작업이 실행되지 못할 수 있다.
- 람다의 클로저 캡처링과 유사하게 내부 상태를 클래스 필드로 캡처한다.
이것 때문에, 코투린의 Dispatcher.IO는 블로킹 작업에서 사용된다.
- 블로킹 작업은 제어권을 넘기는 데 시간이 오래 걸리므로, 이러한 작업에 충분한 쓰레드를 제공하기 위해 쓰레드 풀이 많도록 설계되어 있다.
- 블로킹 작업 중 다른 코루틴이 실행되지 못하더라도, 여러 쓰레드가 존재하므로 작업 병렬 처리가 가능하다.
또한 구조화된 동시성을 통해 코루틴의 범위를 한정지어 관리가 가능하다.
스프링 MVC에서 코루틴 + WebClient 사용시 주의사항
스프링 MVC RestController에서
코프링은 suspend를 사용해도 요청처리시 일반적인 MVC와 같이 쓰레드풀로부터 요청을 받아 처리하게 된다.
그러나, 컨트롤러의 suspend를 그대로 사용하여 WebClient와 같은 NIO 기술을 JPA와 함께 사용해서는 절대 안된다
@RestController
class TestController(
private val userService: UserService
) {
@GetMapping("/test")
fun test(): List<UserDto> {
log.info("[${Thread.currentThread().name}] test called ")
val data = userService.findAll()
log.info("[${Thread.currentThread().name}] test end ")
return data
}
@GetMapping("/suspend/test")
suspend fun suspendTest(): List<UserDto> {
log.info("[${Thread.currentThread().name}]suspend test called ${coroutineContext}")
val data = userService.suspendFindAll()
log.info("[${Thread.currentThread().name}] suspend test end ${coroutineContext}")
return data
}
@GetMapping("/suspend/testForDispatchers")
suspend fun suspendTestForDispatchers(): List<UserDto> {
log.info("[${Thread.currentThread().name}] suspend test called ${coroutineContext}")
val data = withContext(Dispatchers.IO){
userService.suspendFindAll()
}
log.info("[${Thread.currentThread().name}] suspend test end ${coroutineContext}")
return data
}
val log = org.slf4j.LoggerFactory.getLogger(TestController::class.java)
}
suspend fun ApiClient.callMany(
times: Int = 3,
): List<String> = coroutineScope {
val req = (1..times).map {
async {
test()
}
}
req.awaitAll()
}
@Component
class ApiClient{
val webClient = WebClient.builder()
.baseUrl("http://localhost:3000")
.build()
suspend fun test(): String {
println("[${Thread.currentThread().name}] api called ${coroutineContext}")
val data = webClient.get()
.uri("/test")
.retrieve()
.awaitBody<String>()
println("[${Thread.currentThread().name}] api end ${coroutineContext}")
return data
}
val log = org.slf4j.LoggerFactory.getLogger(ApiClient::class.java)
}
webClient를 통해, 논블로킹 API를 사용한다.
callMany함수를 통해, 동시에 3번의 요청을 하는 상황을 가정해보자
@Service
class UserService(
private val userRepository: UserRepository,
private val apiClient: ApiClient
) {
fun findAll(): List<UserDto> {
log.info("before db")
userRepository.findAll()
log.info("after db")
runBlocking {
log.info("before Many Call ${kotlin.coroutines.coroutineContext}")
apiClient.callMany()
log.info("After Many Call ${kotlin.coroutines.coroutineContext}")
}
log.info("before db")
val db = userRepository.findAll()
log.info("after db")
return db.map { UserDto(it.id, it.name) }
}
suspend fun suspendFindAll(): List<UserDto> {
println("[${Thread.currentThread().name}] before db called ${coroutineContext}")
userRepository.findAll()
println("[${Thread.currentThread().name}] after db called ${coroutineContext}")
println("[${Thread.currentThread().name}] before Many Call ${coroutineContext}")
apiClient.callMany()
println("[${Thread.currentThread().name}] after Many Call ${coroutineContext}")
println("[${Thread.currentThread().name}] before db called ${coroutineContext}")
val db = userRepository.findAll()
println("[${Thread.currentThread().name}] after db called ${coroutineContext}")
log.info("after db ${coroutineContext}")
return db.map { UserDto(it.id, it.name) }
}
val log = org.slf4j.LoggerFactory.getLogger(UserService::class.java)
}
1. runBlocking을 통해 WebClient 호출
2. 컨트롤러의 suspend 내부에서 runBlocking없이 그대로 WebClient 호출 후, 블로킹 작업 진행
3. 컨트롤러의 suspend 내부에서 Dispatcher.IO에서 suspend 함수 호출
1번: DB호출시 전후 모두 [http-nio-8080-exec-2]
2024-11-28T15:42:18.623+09:00 INFO 6238 --- [nio-8080-exec-2] c.reditus.webclienttest.TestController : [http-nio-8080-exec-2] test called
2024-11-28T15:42:18.623+09:00 INFO 6238 --- [nio-8080-exec-2] com.reditus.webclienttest.UserService : before db
2024-11-28T15:42:18.624+09:00 DEBUG 6238 --- [nio-8080-exec-2] org.hibernate.SQL :
select
u1_0.id,
u1_0.name
from
users u1_0
2024-11-28T15:42:18.625+09:00 INFO 6238 --- [nio-8080-exec-2] com.reditus.webclienttest.UserService : after db
2024-11-28T15:42:18.625+09:00 INFO 6238 --- [nio-8080-exec-2] com.reditus.webclienttest.UserService : before Many Call [BlockingCoroutine{Active}@2c7ba80c, BlockingEventLoop@74c218b3]
[http-nio-8080-exec-2] api called [DeferredCoroutine{Active}@5d00b945, BlockingEventLoop@74c218b3]
[http-nio-8080-exec-2] api called [DeferredCoroutine{Active}@5a721a83, BlockingEventLoop@74c218b3]
[http-nio-8080-exec-2] api called [DeferredCoroutine{Active}@162d8b10, BlockingEventLoop@74c218b3]
[http-nio-8080-exec-2] api end [DeferredCoroutine{Active}@5d00b945, BlockingEventLoop@74c218b3]
[http-nio-8080-exec-2] api end [DeferredCoroutine{Active}@5a721a83, BlockingEventLoop@74c218b3]
[http-nio-8080-exec-2] api end [DeferredCoroutine{Active}@162d8b10, BlockingEventLoop@74c218b3]
2024-11-28T15:42:19.734+09:00 INFO 6238 --- [nio-8080-exec-2] com.reditus.webclienttest.UserService : After Many Call [BlockingCoroutine{Active}@2c7ba80c, BlockingEventLoop@74c218b3]
2024-11-28T15:42:19.735+09:00 INFO 6238 --- [nio-8080-exec-2] com.reditus.webclienttest.UserService : before db
2024-11-28T15:42:19.737+09:00 DEBUG 6238 --- [nio-8080-exec-2] org.hibernate.SQL :
select
u1_0.id,
u1_0.name
from
users u1_0
2024-11-28T15:42:19.738+09:00 INFO 6238 --- [nio-8080-exec-2] com.reditus.webclienttest.UserService : after db
2024-11-28T15:42:19.738+09:00 INFO 6238 --- [nio-8080-exec-2] c.reditus.webclienttest.TestController : [http-nio-8080-exec-2] test end
2번 첫 DB호출 [http-nio-8080-exec-3]/ 두번째 DB 호출 [reactor-http-nio-7]
2024-11-28T15:42:47.659+09:00 INFO 6238 --- [nio-8080-exec-3] c.reditus.webclienttest.TestController : [http-nio-8080-exec-3]suspend test called [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
[http-nio-8080-exec-3] before db called [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
2024-11-28T15:42:47.660+09:00 DEBUG 6238 --- [nio-8080-exec-3] org.hibernate.SQL :
select
u1_0.id,
u1_0.name
from
users u1_0
[http-nio-8080-exec-3] after db called [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
[http-nio-8080-exec-3] before Many Call [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
[http-nio-8080-exec-3] api called [Context0{}, DeferredCoroutine{Active}@22b0edb9, Dispatchers.Unconfined]
[http-nio-8080-exec-3] api called [Context0{}, DeferredCoroutine{Active}@7bf8f0db, Dispatchers.Unconfined]
[http-nio-8080-exec-3] api called [Context0{}, DeferredCoroutine{Active}@656b2fa7, Dispatchers.Unconfined]
[reactor-http-nio-6] api end [Context0{}, DeferredCoroutine{Active}@7bf8f0db, Dispatchers.Unconfined]
[reactor-http-nio-5] api end [Context0{}, DeferredCoroutine{Active}@22b0edb9, Dispatchers.Unconfined]
[reactor-http-nio-7] api end [Context0{}, DeferredCoroutine{Active}@656b2fa7, Dispatchers.Unconfined]
[reactor-http-nio-7] after Many Call [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
[reactor-http-nio-7] before db called [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
2024-11-28T15:42:48.773+09:00 DEBUG 6238 --- [ctor-http-nio-7] org.hibernate.SQL :
select
u1_0.id,
u1_0.name
from
users u1_0
[reactor-http-nio-7] after db called [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
2024-11-28T15:42:48.776+09:00 INFO 6238 --- [ctor-http-nio-7] com.reditus.webclienttest.UserService : after db [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
2024-11-28T15:42:48.776+09:00 INFO 6238 --- [ctor-http-nio-7] c.reditus.webclienttest.TestController : [reactor-http-nio-7] suspend test end [Context0{}, MonoCoroutine{Active}@c184e13, Dispatchers.Unconfined]
3번 첫 DB호출 [DefaultDispatcher-worker-1]/ 두번째 DB 호출 [DefaultDispatcher-worker-3]
2024-11-28T15:43:09.764+09:00 INFO 6238 --- [nio-8080-exec-5] c.reditus.webclienttest.TestController : [http-nio-8080-exec-5] suspend test called [Context0{}, MonoCoroutine{Active}@31ccda39, Dispatchers.Unconfined]
[DefaultDispatcher-worker-1] before db called [Context0{}, DispatchedCoroutine{Active}@31836e76, Dispatchers.IO]
2024-11-28T15:43:09.770+09:00 DEBUG 6238 --- [atcher-worker-1] org.hibernate.SQL :
select
u1_0.id,
u1_0.name
from
users u1_0
[DefaultDispatcher-worker-1] after db called [Context0{}, DispatchedCoroutine{Active}@31836e76, Dispatchers.IO]
[DefaultDispatcher-worker-1] before Many Call [Context0{}, DispatchedCoroutine{Active}@31836e76, Dispatchers.IO]
[DefaultDispatcher-worker-3] api called [Context0{}, DeferredCoroutine{Active}@17a77cf1, Dispatchers.IO]
[DefaultDispatcher-worker-4] api called [Context0{}, DeferredCoroutine{Active}@7278330b, Dispatchers.IO]
[DefaultDispatcher-worker-2] api called [Context0{}, DeferredCoroutine{Active}@5108d252, Dispatchers.IO]
[DefaultDispatcher-worker-4] api end [Context0{}, DeferredCoroutine{Active}@5108d252, Dispatchers.IO]
[DefaultDispatcher-worker-2] api end [Context0{}, DeferredCoroutine{Active}@17a77cf1, Dispatchers.IO]
[DefaultDispatcher-worker-3] api end [Context0{}, DeferredCoroutine{Active}@7278330b, Dispatchers.IO]
[DefaultDispatcher-worker-3] after Many Call [Context0{}, DispatchedCoroutine{Active}@31836e76, Dispatchers.IO]
[DefaultDispatcher-worker-3] before db called [Context0{}, DispatchedCoroutine{Active}@31836e76, Dispatchers.IO]
2024-11-28T15:43:10.882+09:00 DEBUG 6238 --- [atcher-worker-3] org.hibernate.SQL :
select
u1_0.id,
u1_0.name
from
users u1_0
[DefaultDispatcher-worker-3] after db called [Context0{}, DispatchedCoroutine{Active}@31836e76, Dispatchers.IO]
2024-11-28T15:43:10.884+09:00 INFO 6238 --- [atcher-worker-3] com.reditus.webclienttest.UserService : after db [Context0{}, DispatchedCoroutine{Active}@31836e76, Dispatchers.IO]
2024-11-28T15:43:10.885+09:00 INFO 6238 --- [atcher-worker-3] c.reditus.webclienttest.TestController : [DefaultDispatcher-worker-3] suspend test end [Context0{}, MonoCoroutine{Active}@31ccda39, Dispatchers.Unconfined]
suspend 함수는 코루틴을 중단(suspend) 할 수 있게 해주는 함수로, 해당 함수 내에서 I/O 작업이나 비동기 작업을 수행하는 경우에 유용하다.
코루틴이 중단되면, 해당 스레드는 다른 작업을 처리할 수 있도록 반납되며, 다른 코루틴이 해당 스레드를 사용할 수 있게 된다. 이로 인해 비동기적으로 여러 작업을 동시에 처리할 수 있게 되며, 효율적인 리소스 사용이 가능해진다.
스프링 MVC에서 컨트롤러의 suspend 함수는 기본적으로 디스패처가 Dispatchers.Unconfined로 지정된다.
디스패처를 명시적으로 지정하면, 해당 작업이 다른 스레드에서 처리되며, 현재 스레드를 반납하여 다른 작업을 처리할 수 있게 된다.
Dispatchers.Unconfined에서는 코루틴이 일시 중단 이후 재개된다면 자신을 재개시키는 스레드에서 실행된다.
즉, 스레드를 반납하지 않고 계속 사용하게 된다. 요청이 완료될 때까지 스레드가 점유된 채로 유지되며, 다른 작업을 할 수 없게 되는 문제가 발생할 수 있는 것이다.
WebClient는 비동기적으로 작동하지만, 해당 요청이후 스레드가 반납되지 않는다면 reactor의 NIO 스레드가 계속 사용된다.
만약 NIO 스레드가 JPA 호출로 블로킹된다면 다른 요청을 처리할 수 없다. 이 경우, NIO 스레드가 JPA 작업으로 인해 블로킹되고, 이 스레드에서 WebClient의 비동기 호출이 더 이상 효율적으로 실행되지 못한다.
runBlocking을 사용하여 JPA와 호환되도록
Spring MVC에서 코루틴을 효율적으로 사용하자.
코루틴 CoroutineScope 정리
구조화된 동시성은 어떻게 상속되는 걸까
1. CoroutineScope는 CoroutineContext를 가지고 있다.
CoroutineScope() 함수는 CoroutineContext를 인자로 받아서 코루틴 스코프를 만들 수 있다.
2. launch와 같은 코루틴 빌더는 CoroutineScope의 확장함수이고, Context를 첫번째 인자로 받는다.
이후 수신객체인 CoroutineScope의 CoroutineContext에 입력받은 Context를 더한다.(기본값은 Empty이다)
코루틴 빌더 함수(block)가 실행되고, 새로 변경된 Context의 잡을 부모로 하는 신규 Job을 만들어 해당 Context에 더한다.
이렇게 만들어진 Job을 리턴하는 것이다.
해당 과정을 통해 코루틴의 구조화가 상속이 된다.
수신객체를 통한 신기한 상속작업인거같다.
이를 통해 XXScope(DIspatcher.IO).launch{}를 하거나, xxScopre.launch(Dispathcer.IO)를 하거나 하면 해당 디스패처의 성질이 밑으로 계속 내려가는 것을 알게되었다.
수동으로 만드는 Job()은 코루틴 끝에서 job.complete()를 호출해야 메모리누수가 일어나지 않는다.
---
plus
suspend fun test() = coroutineScope{}
해당함수에서는 컨텍스트를 전달못하는 것으로 보인다.
어떻게 구조화가 상속되는걸까?
suspend함수는 함수를 호출할때, 컨티뉴에이션 객체를 전달한다.
해당 컨티뉴에이션 객체가 컨텍스트를 참조하기 때문에, suspend에서 부모의 컨텍스트에 접근하는 것이 가능한 것이다.
suspend fun hello3() {
println("Hello, Coroutines!")
}
@Nullable
public static final Object hello3(@NotNull Continuation $completion) {
String var1 = "Hello, Coroutines!";
System.out.println(var1);
return Unit.INSTANCE;
}
@kotlin.SinceKotlin public interface Continuation<in T> {
public abstract val context: kotlin.coroutines.CoroutineContext
public abstract fun resumeWith(result: kotlin.Result<T>): kotlin.Unit
}
'Spring' 카테고리의 다른 글
[Spring] Kotlin으로 Spring AOP 우회 DSL 만들기 (1) | 2024.11.25 |
---|