[Akka] Actor Remoting

smlee·2023년 11월 21일
0

Akka

목록 보기
49/50
post-thumbnail

이전까지 Akka를 공부하며 Actor는 다음과 같은 특징을 갖는다는 것을 공부했었다.

  1. Actor는 메시지 기반으로 소통한다.
  2. 각각의 Actor는 캡슐화되어 있으며, Reference(ActorRef)를 통해 소통한다.
  3. at most once delivery 기반
  4. (sender, receiver) 쌍을 기준으로 메시지 순서가 보장된다.

위와 같은 Actor의 규칙들은 같은 JVM 상에 있는 병렬 어플리케이션이나, 로컬의 multiple JVM 환경, 어떤 스케일이든 상관 없는 분산 환경에서 적용된다.

Local Transparency (위치 투명성) & Transparent remoting
Akka에서 위치 투명성이란 액터는 reference를 통해 소통하므로 실제 액터는 어디에든 있어도 된다.는 개념이다. 즉, 어디에 있든 접근할 수 있다는 것을 뜻한다.
여기서 헷갈리지 말아야 하는 개념은 transparent remoting이다. transparent remoting은 로컬에 있는 것처럼 사용하는 것이며

1. config

우리는 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로 설정하였다.

2. 서로 다른 JVM의 ActorSystem

서로 다른 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번까지의 코드를 실행하면 어떻게 될까?

위의 이미지처럼 RemoteAppLocalApp이 실행된다. 따라서 다른 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에 메시지를 보내려면 어떻게 해야 할까?

3. 다른 시스템의 액터에 메시지 보내기

(1) ActorSelection 이용하기

우리는 이전에 공부했던 내용 중에서 ActorSelection(액터 경로)를 통해 해당 액터에 메시지를 보낼 수 있다. 위에서 출력된 RemoteSystem의 경로인 akka://RemoteSystem@localhost:2552remoteActor의 위치인 /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이 제대로 전달된 것을 볼 수 있다.

(2) resolveOne 이용하기

resolveOneActorRef를 가져온다. 하지만, 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만 사용한 것과 같다.

(3) Identify, ActorIdentity 메시지 이용하기

Identify(메시지)ActorIdentity(메시지, actorRef) 메시지를 사용하는 방법이 가장 권장하는 방법이다. actor 친화적이기 때문이다. 이 방법을 설정하기 위해서는 다음과 같은 액터를 추가로 선언해야 한다.

위의 IdentifyActorIdentityakka.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"

위의 코드를 실행하면 다음과 같다.

0개의 댓글