핑퐁 액터는 액터를 만들과 실행하기 위한 가장 기본적인 방법이다. 이는 Akka의 Hello World와 같은 예제이다.
예제 코드를 먼저 작성하고, 이들을 분석하면서 정리해나갈 예정이다.
우리는 다음과 같은 Akka 프로그램을 설계할 것이다.
(1) "핑"이라는 액터와 "퐁"이라는 액터가 존재한다.
(2) 핑은 퐁으로부터 100번의 메시지를 받고 싶다. 따라서 퐁으로부터 메시지를 받으면 countDown을 줄여나가고 Ping한테 메시지를 보낼 것이다. 그리고 메시지를 받았다는 로그를 출력할 예정이다.
(3) 퐁이 핑으로부터 메시지를 받았다면 메시지를 받았다는 로그를 출력하고 핑에게 메시지를 다시 보낼 것이다.
object Main extends App {
val system = ActorSystem("ping-pong_game")
val pinger = system.actorOf(Props[PingActor](), "pinger")
val ponger = system.actorOf(Props(classOf[PongActor], pinger), "ponger")
system.scheduler.scheduleOnce(500 millis){
ponger ! Ping
}
}
ActorSystem
객체를 선언하여 어떤 액터시스템인지 명명한다. (이때, ActorSystem명에는 공백이 들어올 수 없다.) 그리고, Actor들을 불러온 후 메시지 보내기를 실행시킨다.
이때 reciever ! 보낼 것
순으로 메시지를 보낸다. 즉, 위의 코드에서는 ponger에게 Ping이라는 메시지를 보낸 것이다.
Props는 Configuration class로, 액터의 생성에 관한 옵션들을 구체화한다. 따라서 immutable
이라고 생각하면 된다.
import akka.actor.Props
val props1 = Props[MyActor]()
val props2 = Props(new ActorWithArgs("arg"))
val props3 = Props(classOf[ActorWithArgs], "arg") // no support for value class arguments
만약 생성자에서 파라미터가 필요하지 않다면 props1
처럼 선언하고, 파라미터가 필요하다면 props3
처럼 선언되는 것이 좋다. 그리고, props2
처럼 선언하는 것은 지양하는 것이 좋다.
case object Pong
case object Ping
class PingActor extends Actor{
private val log = Logging(context.system, this)
private var countDown:Int = 100
override def receive: Receive = {
case Pong =>
log.info(s"${self.path} received pong, count down $countDown")
if (countDown > 0) {
countDown -= 1
sender() ! Ping
} else {
sender() ! PoisonPill
self ! PoisonPill
}
}
}
PingActor
는 메시지를 주고 받을 횟수와 메시지를 보내는 역할을 한다.
이때, Actor
는 Actor라는 trait를 extend시키고 receive
메서드를 오버라이드 함으로써 액터를 정의할 수 있다. 이때, Akka Actor의 receive
메시지 루프는 패턴 매칭을 통해 진행되어야 한다. receive 메서드의 결과는 부분 함수 객체이다.
여기서, PingActor는 var
이라는 공유 변수를 사용했다. 이러면 멀티 스레드 환경에서 문제가 생기지 않을까 생각할수 있지만 Akka가 Actor Model
을 채용하고 있어, 외부 Actor나 객체, 스레드에서 다른 Actor의 내부에 관여할 수 없이 때문에 안정성을 지니게 된다.
그리고 눈에 띄는 것은 PoisonPill
이다. 이는 Stop
이나 Kill
과 유사하게 동작한다. 즉, 액터를 종료시키고 메시지 큐를 멈추는 것이다.
위의 코드는 Pong이라는 메시지를 받았으면, sender()에게 Ping이라는 메시지를 보내도록 했다.
class PongActor(pinger: ActorRef) extends Actor{
private val log = Logging(context.system, this)
override def receive: Receive = {
case Ping =>
log.info(s"${self.path} received ping")
pinger ! Pong
}
}
PongActor는 전반적으로 PingActor와 유사하면서 더 짧다. 단순히 핑에게서 메시지를 받았으면 메시지를 받았다를 로그를 출력하고 메시지를 보내면 되기 때문이다.
여기서 Ping
이나 Pong
이라는 객체는 존재하지 않지만 사용되었다. 그 이유는 (2)번의 PingActor.scala
의 case object로 선언되어 있기 때문이다. Scala에서 default 접근 제어자는 public
이므로 PongActor에서 쉽게 인식 가능하다.
PongActor는 Ping이라는 메시지를 받으면 pinger
에게 Pong이라는 메시지를 주도록 한다.
[INFO] [08/22/2023 11:14:00.202] [ping-pong_game-akka.actor.default-dispatcher-5] [akka://ping-pong_game/user/ponger] akka://ping-pong_game/user/ponger received ping
[INFO] [08/22/2023 11:14:00.205] [ping-pong_game-akka.actor.default-dispatcher-5] [akka://ping-pong_game/user/pinger] akka://ping-pong_game/user/pinger received pong, count down 100
[INFO] [08/22/2023 11:14:00.205] [ping-pong_game-akka.actor.default-dispatcher-5] [akka://ping-pong_game/user/ponger] akka://ping-pong_game/user/ponger received ping
[INFO] [08/22/2023 11:14:00.205] [ping-pong_game-akka.actor.default-dispatcher-6] [akka://ping-pong_game/user/pinger] akka://ping-pong_game/user/pinger received pong, count down 99
(중략)
[INFO] [08/22/2023 11:14:00.247] [ping-pong_game-akka.actor.default-dispatcher-5] [akka://ping-pong_game/user/pinger] akka://ping-pong_game/user/pinger received pong, count down 2
[INFO] [08/22/2023 11:14:00.248] [ping-pong_game-akka.actor.default-dispatcher-4] [akka://ping-pong_game/user/ponger] akka://ping-pong_game/user/ponger received ping
[INFO] [08/22/2023 11:14:00.248] [ping-pong_game-akka.actor.default-dispatcher-5] [akka://ping-pong_game/user/pinger] akka://ping-pong_game/user/pinger received pong, count down 1
[INFO] [08/22/2023 11:14:00.248] [ping-pong_game-akka.actor.default-dispatcher-4] [akka://ping-pong_game/user/ponger] akka://ping-pong_game/user/ponger received ping
[INFO] [08/22/2023 11:14:00.248] [ping-pong_game-akka.actor.default-dispatcher-5] [akka://ping-pong_game/user/pinger] akka://ping-pong_game/user/pinger received pong, count down 0
위의 코드들을 실행하면 위와 같은 결과를 얻을 수 있다.
PingActor
에서 공유 변수를 사용했음에도 올바른 결과를 잘 출력하는 것을 볼 수 있다.
위의 예제들에서 우리는 Props를 통해 Actor들을 생성한 것을 볼 수 있다. 하지만, 이러한 Props를 사용하여 actor들을 생성할 때, 주의해야 할 2가지 케이스가 있다.
case class MyValueClass(v:Int) extends AnyVal
class ValueActor(value:MyValueClass) extends Actor {
override def receive:Receive = {
case multiplier:Long => sender() ! (value.v * multiplier)
}
}
위의 case class
는 AnyVal을 확장한 클래스이며, ValueActor는 이러한 MyValueClass를 파라미터로 받고 있는 액터이다. 그러면 우리는 Props를 사용해 액터를 생성하면 어떻게 될까?
val myValueActor = Props(classOf[ValueActor], MyValueClass(5))
라고 선언할 것이라 생각할 것이다. 하지만, 위의 myValueActor는 생성되지 않고 IllegalArgumentException
이 발생할 것이다.
class DefaultValueActor(a:Int, b:Int = 5) extends Actor{
override def receive:Receive = {
case x:Int => sender() ! ((a+x) * b)
}
}
val defaultValueProp1 = Props(classOf[DefaultValueActor], 2.0)
class DefaultValueActor2(b: Int = 5) extends Actor {
def receive = {
case x: Int => sender() ! (x * b)
}
}
val defaultValueProp2 = Props[DefaultValueActor2]()
val defaultValueProp3 = Props(classOf[DefaultValueActor2])
또한, AnyVal을 사용하지 않더라도 default construct가 있는 class들을 파라미터로 받는 액터들에 대해 위의 defaultValueProp1~3 모두 IllegalArgumentException이 발생한다.
위의 edge case들을 어떻게 처리할까? 우리는 위의 같은 edge case들에 대하여 해당 클래스명과 같은 싱글톤 객체를 선언하여 해결한다. 이는 팩토리 메서드를 싱글톤 객체에 저장한다는 개념과 연관된다.
object DemoActor {
def props(magicNumber: Int): Props = Props(new DemoActor(magicNumber))
}
class DemoActor(magicNumber: Int) extends Actor {
def receive = {
case x: Int => sender() ! (x + magicNumber)
}
}
class SomeOtherActor extends Actor {
context.actorOf(DemoActor.props(42), "demo")
}
즉 위와 같이 DemoActor라는 액터 클래스의 동반 객체를 만들어 해결하는 방법이다.