Actor를 이용하는 방법에는 크게 Untyped Actor와 Typed Actor가 있으며  Typed Actor의 경우 Behavior 패턴을 이용, 처리할 이벤트를 Type정의해 조금더 메시지처리를 엄격하게 관리할수 있습니다.

비교적 최신 스펙인 Typed Actor를 이용 코틀린에서 이용할수 있는 간단한 액터 모델을 생성하고 UnitTest해보겠습니다.


Behavior라는 개념을 채택하였기 때문에 Behaviror의 간단한 설명부터 알아보겠습니다.

  • Behavior 정의는 타입 안정성입니다. 메시지와 그에 대한 응답 유형이 명확하게 정의되므로 컴파일 타임에 타입 오류를 방지할 수 있습니다. 이는 런타임 오류를 줄이고 코드의 안정성을 높여줍니다.
  • Behavior 패턴은 함수형 프로그래밍의 개념을 많이 차용하여 액터의 상태 변화를 명확하게 정의하고, 상태 전이가 더욱 구조적으로 이뤄질 수 있도록 돕습니다.
  • 상태에 따라 액터의 행동이 변화하는 경우, 각 상태별로 명확하게 구분된 Behavior를 정의할 수 있어 코드의 가독성 및 유지보수성이 향상됩니다.
  • Behavior 패턴은 함수형 프로그래밍과 객체지향 프로그래밍의 개념을 적절히 혼합하여 배우기 쉽고 직관적인 API를 제공합니다. 이를 통해 액터의 동작을 코드로 표현하는 방식이 명확해지고 직관적이어서 개발자들이 쉽게 적응할 수 있습니다.
  • Behavior actor는 Akka의 기존 강력한 확장성을 그대로 유지하면서도 더 유연하게 설계할 수 있는 기능을 제공합니다. Behavior를 바꾸는 것이나 메시지를 다른 액터에게 위임하는 것 등이 자유로워, 액터 시스템을 복잡한 분산 시스템에 적응시키기 더 쉽습니다.


HelloActor액터 메시지 흐름설계

  • 행위자를 정의하고  이야기하듯이 이벤트 흐름을 설계할수 있는것이 액터모델의 장점입니다. 
  • 주요 api
    • tell : 이야기합니다.
    • ask : 이야기를 하고 응답을 기다립니다.
    • receive : 이야기를 듣습니다.
    • ActorRef : 액터 참조자로 해당 액터에게 이야기할수 있습니다.

이벤트 정의

/** HelloActor 처리할 수 있는 명령들 */
sealed class HelloActorCommand
data class Hello(val message: String, val replyTo: ActorRef<Any>) : HelloActorCommand()

/** HelloActor 반환할 수 있는 응답들 */
sealed class HelloActorResponse
data class HelloResponse(val message: String) : HelloActorResponse()

명령 및 응답 정의

  • HelloActorCommand: HelloActor가 처리할 수 있는 명령을 나타내는 sealed class입니다.
  • Hello: HelloActor가 처리할 수 있는 명령 중 하나로, 메시지와 응답을 보낼 액터 참조를 포함합니다.
  • HelloActorResponse: HelloActor가 반환할 수 있는 응답을 나타내는 sealed class입니다.
  • HelloResponse: HelloActor가 반환할 수 있는 응답 중 하나로, 메시지를 포함합니다.


HelloActor 구현

HelloActor
import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.javadsl.AbstractBehavior
import akka.actor.typed.javadsl.ActorContext
import akka.actor.typed.javadsl.Behaviors
import akka.actor.typed.javadsl.Receive

/** HelloActor 클래스 */
class HelloActor private constructor(
    private val context: ActorContext<HelloActorCommand>,
) : AbstractBehavior<HelloActorCommand>(context) {

    companion object {
        fun create(): Behavior<HelloActorCommand> {
            return Behaviors.setup { context -> HelloActor(context) }
        }
    }

    override fun createReceive(): Receive<HelloActorCommand> {
        return newReceiveBuilder()
            .onMessage(Hello::class.java, this::onHello)
            .build()
    }

    private fun onHello(command: Hello): Behavior<HelloActorCommand> {

        if (command.message == "Hello") {
            command.replyTo.tell(HelloResponse("Kotlin"))
        }
        return this
    }
}
  • 수신정의된 이벤트를 처리할 함수와 바인딩 처리할수 있습니다.
  • 'Hello'를 수신받으면 'Kotlin' 으로 응답하는 간단한 액터 모델입니다.
  • HelloActor는 AbstractBehavior<HelloActorCommand>를 상속받아 구현됩니다.
  • create 메서드는 HelloActor의 인스턴스를 생성하는 팩토리 메서드입니다.
  • createReceive 메서드는 액터가 수신할 메시지를 정의합니다.
  • onHello 메서드는 Hello 명령을 처리하고, 메시지가 "Hello"인 경우 HelloResponse("Kotlin")을 응답으로 보냅니다.


HelloActorTest 

HelloActorTest 
import akka.actor.testkit.typed.javadsl.ActorTestKit
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

class HelloActorTest {

    companion object {
        private lateinit var testKit: ActorTestKit

        @BeforeAll
        @JvmStatic
        fun setup() {
            testKit = ActorTestKit.create()
        }

        @AfterAll
        @JvmStatic
        fun teardown() {
            testKit.shutdownTestKit()
        }
    }

    @Test
    fun testHelloActorRespondsWithKotlin() {
        val probe = testKit.createTestProbe<Any>()

        val helloActor = testKit.spawn(HelloActor.create())

        helloActor.tell(Hello("Hello", probe.ref()))

        probe.expectMessage(HelloResponse("Kotlin"))
    }
}



TypedActor의 경우 UntypedActor와 비교 코드 리팩토리를 할수 있는 장점도 있습니다.

TypeCheck를 하지 않는 액터(Untyped)경우 실행타임에 처리할수 없는 메시지를 알수 있지만

TypedActor의 경우 코드수정 단계에서 알수 있으며 OOP의 특성을 적극 활용할수 있습니다.



Copilot 응용편

기본 모델을 작성후 Copilot를 활용해 액터모델을 업데이트 할수 있습니다.


Hello를 응답할때마다 카운트를 증가시키고~ 몇번 응답을 했는지 알려주는 Command도 생성


추가되는 코드

/** 이벤트 정의 */

data class GetHelloCount(val replyTo: ActorRef<Any>) : HelloActorCommand()
data class HelloCountResponse(val count: Int) : HelloActorResponse()

/** HelloActor 클래스 */
class HelloActor private constructor(
    private val context: ActorContext<HelloActorCommand>,
) : AbstractBehavior<HelloActorCommand>(context) {

    override fun createReceive(): Receive<HelloActorCommand> {
        return newReceiveBuilder()
            .onMessage(Hello::class.java, this::onHello)
            .onMessage(GetHelloCount::class.java, this::onGetHelloCount)
            .build()
    }      private var helloCount: Int = 0

    private fun onHello(command: Hello): Behavior<HelloActorCommand> {
        if (command.message == "Hello") {
            helloCount++
            command.replyTo.tell(HelloResponse("Kotlin"))
        }
        return this
    }

    private fun onGetHelloCount(command: GetHelloCount): Behavior<HelloActorCommand> {
        command.replyTo.tell(HelloCountResponse(helloCount))
        return this
    }

/** 추가되는 유닛테스트 코드 */

        helloActor.tell(GetHelloCount(probe.ref()))

        probe.expectMessage(HelloCountResponse(1))


git : https://github.com/psmon/java-labs/blob/master/KotlinBootLabs/src/test/kotlin/actor/HelloActorTest.kt

Copilot를 활용해 코틀린에서 유용한 액터모델을  생성하는 실험을 이어가보겠습니다.






  • No labels