이전까지 Akka를 공부하며 Actor는 다음과 같은 특징을 갖는다는 것을 공부했었다.
위와 같은 Actor의 규칙들은 같은 JVM 상에 있는 병렬 어플리케이션이나, 로컬의 multiple JVM 환경, 어떤 스케일이든 상관 없는 분산 환경에서 적용된다.
Local Transparency (위치 투명성) & Transparent remoting
Akka에서 위치 투명성이란 액터는 reference를 통해 소통하므로 실제 액터는 어디에든 있어도 된다.는 개념이다. 즉, 어디에 있든 접근할 수 있다는 것을 뜻한다.
여기서 헷갈리지 말아야 하는 개념은transparent remoting
이다. transparent remoting은 로컬에 있는 것처럼 사용하는 것이며
우리는 Actor Remoting을 위한 config가 필요하다. resources 디렉터리 내에
localSystem {
akka {
actor {
provider = remote
}
}
remote {
artery {
enabled = on
transport = aeron-udp
canonical.hostname = "localhost"
canonical.port = 2551
}
}
}
remoteSystem {
akka {
actor {
provider = remote
}
}
remote {
artery {
enabled = on
transport = aeron-udp
canonical.hostname = "localhost"
canonical.port = 2552
}
}
}
우리는 2개의 ActorSystem을 만들고, 각각 다른 JVM 위에서 돌리고 싶다. 따라서 remote
내부의 port를 분리하였다. 그리고 로컬 환경이므로 hostname은 localhost
로 설정하였다.
서로 다른 JVM으로 설정하려면 액터 시스템이 각각 다른 오브젝트에 있어야 한다.
object LocalApp extends App {
val localSystem = ActorSystem("LocalSystem",
ConfigFactory.load("custom_configs/remoteActors.conf").getConfig("localSystem"))
val localSimpleActor = localSystem.actorOf(Props[SimpleActor], "localActor")
localSimpleActor ! "hello local Actor"
}
object RemoteApp extends App {
val remoteSystem = ActorSystem("RemoteSystem",
ConfigFactory.load("custom_configs/remoteActors.conf").getConfig("remoteSystem"))
val remoteSimpleActor = remoteSystem.actorOf(Props[SimpleActor], "remoteActor")
remoteSimpleActor ! "hello remote actor"
}
위와 같이 extends App
이 된 object를 2개 선언하였다. 각각의 앱에서 각자의 액터 시스템이 존재한다. 이때, SimpleActor는 밑과 같이 단순히 액터가 받은 메시지와 메시지를 보낸 발신인을 출력하는 액터이다.
class SimpleActor extends Actor with ActorLogging {
override receive: Receive = {
case message = log.info(s"Received message: $message from ${sender()}")
}
}
이제 2번까지의 코드를 실행하면 어떻게 될까?
위의 이미지처럼 RemoteApp
과 LocalApp
이 실행된다. 따라서 다른 JVM 상에 각각 노드가 올라간다.
[INFO] [11/21/2023 17:39:10.460] [main] [akka.remote.artery.aeron.ArteryAeronUdpTransport(akka://LocalSystem)] Remoting started with transport [Artery aeron-udp]; listening on address [akka://LocalSystem@localhost:2551]
그리고 위의 log.info
내부의 내용을 보면 현재 로컬 시스템의 경로는 akka://LocalSystem@localhost:2551
이다. 즉, akka://
뒤에 붙는 단어인 LocalSystem
은 액터 시스템 명이며 @
뒤의 localhost:2551
은 config 파일에서 설정한 Hostname과 Port이다.
이제 여기서 LocalSystem
내부에서 RemoteSystem
내부에 있는 remoteSimpleActor
에 메시지를 보내려면 어떻게 해야 할까?
우리는 이전에 공부했던 내용 중에서 ActorSelection(액터 경로)
를 통해 해당 액터에 메시지를 보낼 수 있다. 위에서 출력된 RemoteSystem의 경로인 akka://RemoteSystem@localhost:2552
와 remoteActor
의 위치인 /user/remoteActor
를 합치면 actorSelection의 경로가 된다.
object LocalApp extends App {
val localSystem = ActorSystem("LocalSystem",
ConfigFactory.load("part2_remoting/remoteActors.conf").getConfig("localSystem"))
val localSimpleActor = localSystem.actorOf(Props[SimpleActor], "localActor")
localSimpleActor ! "hello local Actor"
val remoteActor = localSystem.actorSelection("akka://RemoteSystem@localhost:2552/user/remoteActor")
remoteActor ! "hello from local"
}
위의 코드처럼 다른 시스템에 존재하더라도 위치를 정확히 지정하면 메시지를 보낼 수 있다.
위의 이미지에서 가장 아래 쪽의 로그를 보면 hello from local
이 제대로 전달된 것을 볼 수 있다.
resolveOne
은 ActorRef
를 가져온다. 하지만, Future를 사용하므로 조금 느릴 수 있다.
import localSystem.dispatcher
implicit val timeout = Timeout(3 seconds)
val remoteSimpleActorPath = localSystem.actorSelection("akka://RemoteSystem@localhost:2552/user/remoteActor")
val remoteActorFuture = remoteSimpleActorPath.resolveOne()
remoteActorFuture.onComplete{
case Success(actorRef) => actorRef ! "hello from local"
case Failure(exception) => println(s"failed because of $exception")
}
이 방법 역시 actorSelection
을 사용하지만, resolveOne
을 통하여 ActorRef
를 가져와 직접 레퍼런스와 통신한다.
실행 결과는 actorSelection
만 사용한 것과 같다.
Identify(메시지)
와 ActorIdentity(메시지, actorRef)
메시지를 사용하는 방법이 가장 권장하는 방법이다. actor 친화적이기 때문이다. 이 방법을 설정하기 위해서는 다음과 같은 액터를 추가로 선언해야 한다.
위의 Identify
와 ActorIdentity
는 akka.actor
패키지 내의 특수한 메시지로, Identify(메시지)
를 보내면 해당 메시지를 받은 Actor는 ActorIdentity(메시지, actorRef)
를 리턴한다. 이때, 메시지는 Identify
메시지 내부 소괄호에 든 메시지와 같은 내용이며, actorRef에 자신의 액터 ref를 리턴시킨다.
class ActorResolver extends Actor with ActorLogging {
override def preStart(): Unit = {
val remoteActor = context.actorSelection("akka://RemoteSystem@localhost:2552/user/remoteActor")
remoteActor ! Identify(42)
}
override def receive:Receive = {
case ActorIdentity(_, Some(actorRef)) => actorRef ! "thank you for identifying yourself"
}
}
위와 같이 actorRef를 받아 거기에서 메시지를 보내는 Resolver를 선언했다. 이를 localapp 내부에서 실행시키면 된다. ActorReolver는 preStart를 오버라이드하여 미리 어떤 액터에 대해 Identify 메시지를 보내놓은 상황이다.
val resolver = localSystem.actorOf(Props[ActorResolver], "resolver")
resolver ! "hello! from local"
위의 코드를 실행하면 다음과 같다.