Kotlin Coroutine과 Spring WebFlux 통합

Kotlin 코루틴과 Spring WebFlux를 통합할 때
발생할 수 있는 주요 문제 중 하나는 비동기 방식의 차이입니다.

코루틴은 Kotlin의 비동기 처리를 위한 기본 메커니즘인 반면,
WebFlux는 Mono, Flux와 같은 리액티브 스트림을 사용합니다.

이 포스팅에서는 Kotlin의 Flow와 WebFlux의 Flux를 함께 사용할 때
발생할 수 있는 변환 비용 및 주의 사항에 대해 알아보겠습니다.

1. 문제설명

Spring WebFlux에서는 비동기 응답을 처리하기 위해 Mono와 Flux를 사용합니다.
반면에, Kotlin의 코루틴에서는 Flow가 비동기 스트림을 처리하는 방식입니다.
두 시스템을 함께 사용하면, 변환 과정에서 성능 비용이 발생할 수 있습니다.

Request Body를 처리하고 Response로 JSON 반환하기

suspend fun handleRequest(request: ServerRequest): ServerResponse {
    val body = request.bodyToMono<String>().awaitSingle()
    val jsonResponse = processRequestBody(body)
    
    return ServerResponse.ok().bodyValueAndAwait(jsonResponse)
}

위 코드는 Spring WebFlux에서 bodyToMono로 요청의 Body를 받아오는 코드입니다.

awaitSingle()은 Mono를 Kotlin 코루틴에서 suspend 함수로 처리할 수 있게 변환해줍니다.

하지만 이 과정에서 Mono와 Flow 간의 변환 비용이 발생합니다.

2.코틀린 코루틴과 WebFlux 충돌 문제

WebFlux에서 요청의 Body를 가져와 비동기적으로 처리한 뒤,
응답을 반환할 때 코루틴의 suspend 함수를 사용하고
Mono를 명시적으로 반환하지 않는다면,
WebFlux의 ServerHttpResponse로 이미 응답을 처리한 뒤에도
스프링이 중복된 응답을 보내려고 하여 충돌이 발생할 수 있습니다.

suspend fun problematicHandleRequest(request: ServerRequest): ServerResponse {
    val body = request.bodyToMono<String>().awaitSingle()
    
    // 여기서 이미 응답이 완료되었으나, 추가로 응답을 보내려고 시도
    return ServerResponse.ok().bodyValueAndAwait("Additional Response")
}

위 코드에서는 응답을 이미 ServerHttpResponse를 통해 보냈음에도,
다시 응답을 보내려고 하면서 충돌이 발생할 수 있습니다.

3. Flux와 Flow 변환 비용

Spring WebFlux는 Flux와 Mono라는 리액티브 스트림을 사용하여 비동기 처리를 수행하는데,
Kotlin의 코루틴은 Flow라는 비동기 스트림을 제공합니다.

하지만 Kotlin은 Flux에서 Flow로의 변환을 지원하기 때문에,
코드를 Flux 기반으로 작성하지 않으면 변환 비용이 발생합니다.

// Flux to Flow 변환
suspend fun handleRequestWithFlux(request: ServerRequest): ServerResponse {
    val fluxBody: Flux<String> = request.bodyToFlux()
    
    // Flux를 Flow로 변환
    val flowBody = fluxBody.asFlow()
    
    flowBody.collect { value ->
        println(value)
    }
    
    return ServerResponse.ok().bodyValueAndAwait("Handled with Flow")
}

위 코드에서는 Flux를 Flow로 변환하여 처리하고 있습니다.
이 과정에서 asFlow()로 변환하는 비용이 추가로 발생하며, 성능 문제가 발생할 수 있습니다.

4.디스패처와 스케줄러 혼재 문제

코루틴에서의 디스패처와 WebFlux에서의 스케줄러는 서로 다른 개념이지만,
둘을 혼용할 경우 혼란이 생길 수 있습니다.

특정 비동기 작업에서 컨트롤러마다 다른 디스패처나 스케줄러를 사용할 수 있지만,
이는 성능 및 유지보수 측면에서 문제를 야기할 수 있습니다.

스케줄러와 디스패처 혼용 예시

suspend fun mixedDispatcherExample(request: ServerRequest): ServerResponse {
    withContext(Dispatchers.IO) {
        // I/O 작업을 처리하는 부분
        val result = request.bodyToMono<String>().awaitSingle()
    }
    
    return ServerResponse.ok().bodyValueAndAwait("Completed with mixed dispatchers")
}

이 코드는 Dispatchers.IO를 사용하여 I/O 작업을 처리하고 있습니다.

하지만 Spring WebFlux는 자체 스케줄러를 사용하므로,
디스패처와 스케줄러가 혼재되면 관리가 복잡해질 수 있습니다.

5. 해결책

  • Flux를 우선적으로 사용 : WebFlux의 핵심이 리액티브 스트림(Flux와 Mono)인 만큼, 이를 활용하여 애플리케이션을 작성하는 것이 좋습니다.

  • 변환 비용 최소화 : Flux와 Flow를 혼용하는 대신, 하나의 방식으로 통일하여 사용하는 것이 좋습니다.
    WebFlux 기반에서는 Flux와 Mono를 최대한 활용하고, 코루틴 사용 시에도 변환을 최소화하도록 합니다.

  • 응답 관리 주의: 이미 응답을 보낸 뒤에 또다시 응답을 보내려고 하면 충돌이 발생할 수 있습니다.
    ServerHttpResponse가 이미 응답을 처리했는지 확인하고, 추가적인 응답 처리를 하지 않도록 주의해야 합니다.

6. 결론

Kotlin 코루틴과 Spring WebFlux를 함께 사용할 때는 Flux와 Flow 간의 변환 비용,
응답 처리 충돌, 디스패처와 스케줄러 혼재 문제에 주의해야 합니다.
이를 통해 성능을 최적화하고, 일관된 비동기 코드를 작성할 수 있습니다.

suspend fun optimizedHandleRequest(request: ServerRequest): ServerResponse {
    val body: Mono<String> = request.bodyToMono()

    // Flux나 Mono 기반으로 작성하여 변환 비용 최소화
    val processedBody = body.map { processRequest(it) }

    return ServerResponse.ok().body(processedBody, String::class.java)
}

이 코드는 Mono를 사용하여 요청 Body를 비동기적으로 처리하고 응답을
반환하는 최적화된 방식입니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글