AKKA 프레임워크를 사용한 비헤이버기반 액터모델을 코틀린이 지원하는 순수 액터모델로 변환을 시도
코프링을 이용한다면 로컬전용 이벤트 드리븐방식의 처리기를 만들때 액터모델이 필요할때 채택할수 있습니다. 


AKKA 프레임워크를 이용하는 다양한 언어에서 액터모델 사용법은 다음을 참고 할수 있습니다.



액터모델을 학습하기전 기본언어가 지원하는 동시성 프로그래밍 방식을 먼저 알아두는것은 중요합니다.

액터모델은 기본 언어가 지원하는 동시성 프로그래밍과 병렬프로그래밍을 함께 활용하기때문에 

언어가 지원하는 기본기를 먼저 선행하는것은 중요합니다.


코틀린에서 액터를 사용하기위한 의존모듈

	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
  • 액터모델은 core에 포함되어 있으며 Reactor를 품고있습니다.


파이썬 진영 액터프레임워크 RAY

  • Open AI가 채택해 유명해진 ML 분산처리 프레임워크인 Ray,  Core영역이 왜 액터모델을 채택하고 ML진영에서도 이용하고 있는지?  주시할 필요가 있습니다.
  • 코틀린에서 채택한 코르틴 코어도 액터모델을 기본으로 포함하고 있으며 액터모델을 중심으로 Reactor(리액티브 스트림의 구현체) 로 확장해나가는 기술을 주시할 필요가 있습니다.


Akka의 TypedActor를 코틀린 순수 액터를 사용해 유사하게 작성해보겠습니다.


구현

package com.example.kotlinbootlabs.kactor

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch

sealed class HelloKActorCommand
data class Hello(val message: String, val replyTo: kotlinx.coroutines.CompletableDeferred<HelloKActorResponse>) : HelloKActorCommand()
data class GetHelloCount(val replyTo: kotlinx.coroutines.CompletableDeferred<HelloKActorResponse>) : HelloKActorCommand()

sealed class HelloKActorResponse
data class HelloResponse(val message: String) : HelloKActorResponse()
data class HelloCountResponse(val count: Int) : HelloKActorResponse()

class HelloKActor {

    private val channel = Channel<HelloKActorCommand>()
    private var helloCount = 0
    private val scope = CoroutineScope(Dispatchers.Default)

    init {
        scope.launch {
            for (command in channel) {
                when (command) {
                    is Hello -> handleHello(command)
                    is GetHelloCount -> handleGetHelloCount(command)
                }
            }
        }
    }

    private fun handleHello(command: Hello) {
        if (command.message == "Hello") {
            helloCount++
            command.replyTo.complete(HelloResponse("Kotlin"))
        } else if (command.message == "InvalidMessage") {
            throw RuntimeException("Invalid message received!")
        }
    }

    private fun handleGetHelloCount(command: GetHelloCount) {
        command.replyTo.complete(HelloCountResponse(helloCount))
    }

    suspend fun send(command: HelloKActorCommand) {
        channel.send(command)
    }

    fun stop() {
        scope.cancel()
    }
}
  • AKKA 의 액터객체와 구분하기위해 KActor(코액터) 로 네이밍 ( K-컨텐츠아님)
  • 송수신 Typed를 사용하고 비헤이버 패턴 채택


테스트

package com.example.kotlinbootlabs.kactor
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class HelloKActorTest {
    companion object {
        private const val TIMEOUT_DURATION = 3000L // Timeout duration in milliseconds
    }

    private val actor = HelloKActor()

    @AfterEach
    fun tearDown() {
        actor.stop()
    }

    @Test
    fun testHelloCommand() = runBlocking {

        val response = CompletableDeferred<HelloKActorResponse>()
        actor.send(Hello("Hello", response))
        val result = withTimeout(TIMEOUT_DURATION) { response.await() } as HelloResponse

        //The response message should be 'Kotlin'
        assertEquals("Kotlin", result.message, "Kotlin")
    }

    @Test
    fun testGetHelloCountCommand() = runBlocking {

        val response1 = CompletableDeferred<HelloKActorResponse>()
        actor.send(Hello("Hello", response1))
        withTimeout(TIMEOUT_DURATION) { response1.await() }

        val response2 = CompletableDeferred<HelloKActorResponse>()
        actor.send(GetHelloCount(response2))
        val result = withTimeout(TIMEOUT_DURATION) { response2.await() } as HelloCountResponse
        assertEquals(1, result.count, "The hello count should be 1")
    }

    @Test
    fun testInvalidMessage() {

        val response = CompletableDeferred<HelloKActorResponse>()
        try {
            runBlocking {
                withTimeout(TIMEOUT_DURATION) {
                    actor.send(Hello("InvalidMessage", response))
                }
            }
        } catch (e: RuntimeException) {
            assertEquals("Invalid message received!", e.message, "The exception message should be 'Invalid message received!'")
        }
    }
}
  • Akka test tooli kit 을 이용하는 경우 테스트 중단없는 관찰자(수신검증) 기능을 제공하며, 액터의 수신 검증은  위와같은 코드로 가능합니다.



순수 액터모델 VS AKKA 액터모델

코틀린이 지원하는 액터객체는 액티브오브젝트 패턴 이 적용된 구현체로 Akka액터모델이 지원하는 다양한 부가기능이 없는 순수객체입니다.

Plain Old Java Object, 간단히 POJO는 말 그대로 해석을 하면 오래된 방식의 간단한 자바 오브젝트라는 말로서 Java EE 등의 중량 프레임워크들을 사용하게 되면서 해당 프레임워크에 종속된 "무거운" 객체를 만들게 된 것에 반발해서 사용되게 된 용어이다. 2000년 9월에 마틴 파울러, 레베카 파슨, 조쉬 맥킨지 등이 사용하기 시작한 용어로서 마틴 파울러는 다음과 같이 그 기원을 밝히고 있다. [1]

“    우리는 사람들이 자기네 시스템에 보통의 객체를 사용하는 것을 왜 그렇게 반대하는지 궁금하였는데, 간단한 객체는 폼 나는 명칭이 없기 때문에 그랬던 것이라고 결론지었다. 그래서 적당한 이름을 하나 만들어 붙였더니, 아 글쎄, 다들 좋아하더라고.    ”
     
— 마틴 파울러
POJO라는 용어는 이후에 주로 특정 자바 모델이나 기능, 프레임워크 등을 따르지 않은 자바 오브젝트를 지칭하는 말로 사용되었다. 스프링 프레임워크는 POJO 방식의 프레임워크이다.


순수 패턴(액티브오브젝트)을 이용해 AKKA의존없이 순수 액터모델을 시작으로 메이드 할수 있습니다.

하지만 Akka 액터모델은 CQRS/이벤트소싱 등을 지원하는 완성형 액터를 제공하며, 그 방식을 학습하며  아이디어를 설계에 직접 녹일수도 있습니다.



Akka의 Actor 모델과 Kotlin의 순수 Actor 모델이 지원하는 기능에는 몇 가지 차이가 있습니다. Akka는 고도로 최적화된 액터 프레임워크이며, Kotlin에서는 Akka와 같은 기본적인 Actor 모델이 제공되지 않기 때문에 Kotlin 코루틴을 이용해 직접 구현해야 합니다. 다음은 Akka Actor 모델과 Kotlin 코루틴을 사용한 순수 Actor 모델의 기능 비교입니다:

1. 메시지 전달 및 비동기 처리

  • Akka Actor 모델:
    • 기본적으로 비동기 메시지 전달을 위한 메커니즘이 제공됩니다.
    • tell ask 같은 메시지 전송 방법이 있으며, ask는 요청-응답 패턴을 지원합니다.
    • 아카터들은 메시지 박스를 통해 메시지를 수신하고 처리하여 경쟁 조건을 방지합니다.
  • Kotlin 순수 Actor 모델:
    • produce actor 코루틴 빌더를 통해 비동기 메시지 전달을 구현할 수 있지만, 명시적인 메시지 박스나 큐가 기본적으로 제공되지는 않습니다.
    • 메시지 전달은 코루틴 채널을 통해 수행되며, 수동으로 큐와 같은 구조를 생성해야 안정적인 메시지 처리가 가능합니다.

2. 액터 라이프사이클 관리

  • Akka Actor 모델:
    • 액터의 생성과 종료, 재시도, 중지 등의 라이프사이클 관리가 내장되어 있습니다.
    • supervision 전략을 통해 에러가 발생했을 때 자식 액터를 복구하거나 종료하는 제어가 가능합니다.
    • akka.actor.ActorSystem을 통해 전체 액터 트리와 라이프사이클을 관리합니다.
  • Kotlin 순수 Actor 모델:
    • 수퍼비전 전략이 기본적으로 제공되지 않으므로 에러 처리 및 복구 매커니즘을 직접 구현해야 합니다.
    • CoroutineScope와 SupervisorJob을 활용하면 일부 유사한 구조를 만들 수 있지만 Akka만큼의 제어력은 부족할 수 있습니다.
    • 액터 트리와 같은 구조는 지원되지 않으며, 수동으로 상위-하위 관계를 관리해야 합니다.

3. 상태 관리

  • Akka Actor 모델:
    • 액터의 상태를 관리하는 여러 패턴이 내장되어 있습니다. 예를 들어 PersistentActor로 상태를 저장하거나, Durable State를 활용한 최신 상태 관리, 스냅샷을 통한 복구 최적화 등이 가능합니다.
    • 이벤트 소싱과 스냅샷을 사용한 상태 관리가 기본적으로 제공되며, 복구 전략을 세부적으로 구성할 수 있습니다.
  • Kotlin 순수 Actor 모델:
    • 상태 관리를 위한 직접적인 도구는 제공되지 않으며, 별도의 데이터베이스나 저장소와 함께 수동으로 관리해야 합니다.
    • Akka의 PersistentActor와 같은 기능을 사용하려면 추가적인 라이브러리나 커스텀 구현이 필요합니다.

4. 확장성 및 분산 처리

  • Akka Actor 모델:
    • 클러스터링을 통한 분산 처리를 지원하여 여러 노드에 걸쳐 액터 시스템을 구성할 수 있습니다.
    • Akka Cluster  Sharding 기능으로 대규모 시스템의 확장성과 장애 내성을 강화할 수 있습니다.
  • Kotlin 순수 Actor 모델:
    • 기본적인 코루틴은 JVM 내에서만 동작하므로 분산 처리를 하려면 추가적인 네트워크 통신 및 상태 동기화 메커니즘을 구현해야 합니다.
    • 클러스터링과 같은 기능은 기본적으로 지원되지 않으며, 분산 환경에서 동작하도록 설계하려면 추가적인 개발이 필요합니다.

5. 고급 기능 및 툴링

  • Akka Actor 모델:
    • Akka Streams, Akka HTTP, Akka Persistence와 같은 고급 기능이 제공되어 액터 모델과 자연스럽게 통합할 수 있습니다.
    • 모니터링 및 디버깅을 위한 다양한 도구가 제공되며, Akka의 툴링은 성숙하고 풍부합니다.
  • Kotlin 순수 Actor 모델:
    • 코루틴 자체에는 스트림이나 HTTP 요청 처리 같은 고급 기능이 포함되어 있지 않습니다.
    • 이를 위해 Ktor와 같은 웹 프레임워크, Kotlinx.coroutines.Flow 등을 사용해야 하며, Akka와 같이 일관된 툴링은 제공되지 않습니다.

요약

Akka Actor 모델은 상태 관리, 라이프사이클 제어, 확장성, 고급 툴링에서 강력한 기능을 제공합니다. 반면 Kotlin의 순수 Actor 모델은 단순한 비동기 메시지 전달과 경량 스레드 관리에 초점을 맞추며, 더 많은 개발자가 직접 관리해야 합니다. Akka는 복잡한 도메인과 확장성을 요구하는 대규모 시스템에 적합하며, Kotlin 코루틴을 이용한 순수 Actor 모델은 상대적으로 간단한 동시성 문제 해결에 더 적합합니다.



AKKA가 제공하는 DurableState(영속성) 액터를 코틀린 순수액터모델로 시도를 해보겠습니다.

(AKKA 제공기능에 의존하지말고~ 필요한것은 직접 메이드하는 패턴입니다.)

Next





  • No labels