본문 바로가기
Data Platform/Akka

Akka Classic 뽀개기_Actors(1~4. Actor API)

by 태하팍 2025. 4. 10.
반응형

Classic Actors

이제 Akka Actor에 대해서 살펴보도록 하겠습니다.
우리가 학습해야할 내용은 위의 스크린샷과 같습니다.

1. Module info

먼저 모듈정보는 디펜던시와 같습니다.
스프링프레임워크로보면 maven이나 gradle에서 디펜던시 설정을 통해
필요한 lib를 끌어오는 설정이라고 보시면 됩니다.

2. Introduction

Actor Model은 동시 및 분산 시스템을 작성하기 위한 더 높은 수준의 추상화를 
제공합니다. 개발자가 명시적 잠금 및 스레드 관리를 처리하지 않아도 되므로
올바른 concurrent 및 parallel 시스템을 작성하기가 더 쉬워집니다.
Actor는 1973년 Carl Hewitt의 논문에서 정의되었지만 Erlang 언어로
대중화되었으며 예를 들어 Ericsson에서 매우 동시적이고 안정적인 통신 시스템을 구축하는 데 큰 성공을 거두었습니다.
Akka의 Actors API는 Erlang에서 일부 구문을 차용한
Scala Actors와 유사합니다.

 

3. Creating Actors

참고
Akka에선 액터들이 나무(tree)처럼 계층 구조로 연결되어 있고, 부모가 자식 액터를 책임지고 관리합니다.
그래서 예외처리, 복구 전략(supervision), 액터 참조 구조(Ref, Path, Address) 에 대해 제대로 이해하고 있어야
안정적이고 회복력 있는 시스템을 만들 수 있습니다.

3.1 Defining an Actor class

Akka에서 액터는 Actor 트레이트를 상속하고, 그 안에 receive 메서드를 구현해서 만든다.
receive는 다양한 메시지를 처리할 수 있게 여러 개의 case 문으로 구성되며,
이 전체는 PartialFunction[Any, Unit] 타입이다.
즉, 어떤 메시지를 받을 수 있고, 그 메시지를 받았을 때 어떻게 처리할지를

Scala의 패턴 매칭을 사용해서 구현한다는 뜻이다.

예를 들어보면

import akka.actor.Actor
import akka.actor.Props
import akka.event.Logging

class MyActor extends Actor {
  val log = Logging(context.system, this)

  def receive = {
    case "test" => log.info("received test")
    case _      => log.info("received unknown message")
  }
}

Akka의 액터에서 receive 메시지 루프는 완전한 패턴 매칭을 요구합니다.
이건 Erlang이나 이전 Scala Actor 모델과는 다른 점입니다.
즉, 그 액터가 받을 수 있는 모든 메시지에 대한 패턴 매칭을 명시적으로 작성해야 합니다.
만약 알 수 없는 메시지까지 처리하고 싶다면, 위 예제처럼 기본값 케이스 (case _ =>) 를 넣어야 합니다.
만약 패턴 매칭에 걸리지 않는 메시지를 받으면,
UnhandledMessage 이벤트가 발생해서 ActorSystem의 EventStream에 전파된다.
위에서 정의한 receive 메서드의 반환 타입은 Unit이라는 점도 유의!
액터가 받은 메시지에 응답을 하려면, 명시적으로 ! 연산 등을 통해 직접 응답을 보내야 합니다.
receive 메서드는 PartialFunction 객체를 반환합니다.
이 PartialFunction은 액터의 “초기 행동(initial behavior)“으로 저장됩니다.
액터가 동작 중에 behavior를 바꾸고 싶다면, become / unbecome 개념을 참고하세요!

스칼라기반 Akka는 정말 희한하다 ㅋㅋ
예외처리는 어떻게 하는건지 궁금하고 !연산도 not이 아니라 응답처리 연산이라니..
스칼라답게 객체반환에! 상태변경을 위해 becom/unbecome등 뒷부분에 나올꺼라 기대한다!

3.2 Props

Props는 액터를 생성할 때 사용할 설정을 담는 구성 클래스입니다.
이걸 쉽게 말하면, 액터를 만들기 위한 ‘레시피(설명서)’ 같은 개념인데,
불변(immutable)이기 때문에 안전하게 공유할 수 있습니다.
또한, 액터와 관련된 배포 정보(예: 어떤 dispatcher를 사용할지 등)도 함께 담을 수 있습니다.

아래는 Props 인스턴스를 생성하는 몇 가지 예제입니다.

import akka.actor.Props

val props1 = Props[MyActor]()
val props2 = Props(new ActorWithArgs("arg")) // careful, see below
val props3 = Props(classOf[ActorWithArgs], "arg") // no support for value class arguments

두 번째 방식은 생성될 Actor에게 생성자 인자를 전달하는 방법을 보여주지만, 아래에서 설명하듯 Actor 내부가 아닌 외부에서만
사용해야 합니다.  val props2 = Props(new ActorWithArgs("arg")) // careful, see below
생성자 인자로 직접 인스턴스를 만든 뒤에 Props로 넘겨주는 방식인데 Actor 내부에서 new로 Actor를 만들면
Akka의 Actor 생명주기 관리(감독, 재시작 등)가 깨지기 때문 입니다.
그래서 Actor외부에서만 사용해야 함.
- Actor외부는 main이나 setup을 하는 부분(권장은 하지 않는 방식)
- Actor내부는 특정 Actor의 recevie를 하는 부분(절대 금지!!)
아래의 마지막줄의 방식을 권고한다고 합니다.
즉, Actor 관리는 ActorSystem에서~

마지막 줄(val props3 = Props(classOf[ActorWithArgs], "arg") )은
생성자 인자를 Actor 외부든 내부든 어디에서든 안전하게 전달할 수 있는 방법을 보여줍니다.
이 방식은 classOf와 생성자 인자를 따로 넘겨주며, Akka가 내부적으로 리플렉션을 이용해 일치하는 생성자가 있는지 확인합니다.
이때, 일치하는 생성자가 없거나 두 개 이상이면 IllegalArgumentException이 발생합니다.
그래서 이 방식은 어느 상황에서든 쓸 수 있지만, 전달하는 인자가 정확히 맞는 생성자 하나만 존재해야 합니다.

Props(classOf[ActorWithArgs], "arg") -> Props(classOf[MyActor], "태하팍")
// MyActor라는 클래스를 "태하팍"이라는 인자를 넣어서 생성해달라는 말!

위에서 말한것 같이 Props는 리플렉션을 써서 클래스에 있는 생성자를 찾습니다.
그래서 classOf를 사용해서 클래스 정보를 직접 넘겨주는 방식 입니다.
아래는 자바 방식 입니다.

Props.create(MyActor.class, "태하팍"); // Java 방식

참고

Actor 생성자의 인자로 value class를 사용할 경우, 권장되는 방식(Props 생성 방식)을 사용할 수 없습니다.
위에서 "태하팍"이 생성자의 인자인데 이것을 value class를 넣으면 안된다는 말 입니다. (*3.2.2 참고!)

3.2.1 Dangerous Variants

// NOT RECOMMENDED within another actor:
// encourages to close over enclosing class
val props7 = Props(new MyActor)

2025.03.27 - [Data Platform/Akka] - Akka에서 Actor 생성 시 금지하는 방식

<<주의>>
한 Actor를 다른 Actor 내부에 선언하는 것은 매우 위험하며, Actor의 캡슐화를 깨뜨립니다.
절대 Actor 자신의 this 참조를Props에 넘기지 마세요!
ex) 다른 Actor에게 this를 넘긴다는건 내부 상태를 직접 건드릴수 있게 해줌. 캡슐화 와장창~(위험!)
Actor는 서로 완전히 독립적이어야 함! 자기상태는 자기만 관리!

val props = Props(new MyActor(this))

 

3.2.2 Edge cases(예외 케이스)

akka.actor.Props로 Actor를 생성할 때 두 가지 예외적인 상황이 있습니다.
1) AnyVal 타입의 인자를 가지는 Actor

Value Class의 조건은?

더보기

1. AnyVal을 상속해야 함

2. 필드는 딱 하나만 존재해야 함
3. 생성자에 val 붙여야 함
4. 추상 클래스나 trait이면 안 됨
5. 내부에 var, lazy val, 정의된 equals/hashCode 등이 있으면 안 됨

Value Class는 타입을 명확히 해서 실수를 방지하고자 합니다.

아래의 소스는 value class 입니다.
AnyVal을 상속했으며, 필드는 단 하나만 가집니다.(v: Int)
value class는 클래스인척하지만 사살상은 primitive type의 변수 입니다.
맞습니다! 컴파일러는 아래의 소스를 객체로 만들지 않고 Int처럼 취급합니다.

case class MyValueClass(v: Int) extends AnyVal
class ValueActor(value: MyValueClass) extends Actor {
  def receive = {
    case multiplier: Long => sender() ! (value.v * multiplier)
  }
}
val valueClassProp = Props(classOf[ValueActor], MyValueClass(5)) // Unsupported

결론적으로 아래의 소스에서 MyValueClass(5)를 매개변수로 설정을 하면 위에서 Props를 설명한것 같이
Props는 리플렉션을 써서 인자가 MyValueClass(5)인 클래스(ValueActor)에 있는 생성자를 찾습니다.
문제는 컴파일 타임엔 MyValueClass가 있었으나 런타임에는 MyValueClass가 아닌 Int 입니다.
그래서 못찾아주고 IllegalArgumentException이 발생 합니다.

val valueClassProp = Props(classOf[ValueActor], MyValueClass(5)) // Unsupported


2) 생성자 인자에 기본값(default value)이 있는 Actor 

class DefaultValueActor(a: Int, b: Int = 5) extends Actor {
  def receive = {
    case x: Int => sender() ! ((a + x) * b)
  }
}

val defaultValueProp1 = Props(classOf[DefaultValueActor], 2.0) // Unsupported

class DefaultValueActor2(b: Int = 5) extends Actor {
  def receive = {
    case x: Int => sender() ! (x * b)
  }
}
val defaultValueProp2 = Props[DefaultValueActor2]() // Unsupported
val defaultValueProp3 = Props(classOf[DefaultValueActor2]) // Unsupported
val defaultValueProp1 = Props(classOf[DefaultValueActor], 2.0) // Unsupported

첫번째는 Props이녀석이 역시나 리플렉션을 써서 인자가 2.0인 걸 가지고 DefaultValueActor Class의 생성자를 찾는데
DefaultValueActor Class의 인자는 a: Int형! 2.0인 Double형이라서 unsupported!!
또한 두번째 인자는 기본값으로 5가 들어가있어!
그래서 생략을 한건데 Props(classOf, args..)에서는 기본값을 인식 못해서 unsupported!!

val defaultValueProp2 = Props[DefaultValueActor2]() // Unsupported

두번째로 Props[DefaultValueActor2]() 이 문법은 스칼라 문법을 이해해야합니다.
apply()메소드가 반영되고 있다는걸 눈치까야 합니다! ㅋㅋ
그런데 어떻게 알수가 있을까요??
Props Obejct를 살펴보면 알수 있습니다. (링크)
아래를 보시면 apply가 정의 되어있습니다.

def apply[T <: Actor]()(implicit arg0: ClassTag[T]): Props
Scala API: Returns a Props that has default values except for "creator" which will be a function that creates an instance of the supplied type using the default constructor.

그림_01

그리고 예를 들어 Props[DefaultValueActor2]() 에서
Props가 Object라면 apply() 호출! Class였다면 생성자 호출!

다음 섹션에서는 이런 예외 상황들을 안전하게 피할 수 있는 Actor Props 생성 방법(권장 방식)을 설명합니다.

3.2.3 Recommended Practies

각 Actor의 companion object 안에 factory 메소드(예: props)를 제공하는 것은 좋은 습관입니다.
이렇게 하면 적절한 Props 생성 코드를 Actor 정의 근처에 둘 수 있어서 관리하기 쉽습니다.
그리고 이 방법은 Props.apply(...) (by-name 인자를 받는 방식) 을 쓸 때 발생할 수 있는 문제점들도 피할 수 있습니다.
왜냐하면 companion object 내부에서 정의한 코드 블럭은 외부 스코프(this, 변수 등)를 참조하지 않기 때문입니다.

by-name에 대해서 알아보자!

by-name인자: Props( new MyActor(...) ) // 이 부분은 바로 실행되는게 아니라 Props 내부에서 사용할 때 그때 실행 됨!
이 코드는 값이 아니라 코드 블럭을 함수에 넘기는 방식 입니다.
방식 설명
by-value (x: Int) 함수를 호출할 때 값이 먼저 계산됨
by-name (x: => Int) 함수 안에서 x를 사용할 때마다 계산됨 (실행 지연)

그런데 Props(new MyActor()) 가 by-name이라는데 by-name의 형태는 => A 의 형태랑 다르다?!!
그냥 new로 넘긴거 처럼 보이는데 Why by-name방식이라고 할까요??
이유는 Props객체가 사실상 apply메소드의 파라미터가 by-name인자로 정의 되어있기 때문 입니다.
결론적으로 Props(new MyActor(...)) 이렇게 쓰면 그 안에 정의된 def apply (actor: => Actor): Props 때문에
new MyActor(...)가 by-name으로 전달되는 것 입니다.

object Props {
  def apply(creator: => Actor): Props = ...
}
companion object란?
클래스와 이름이 같은 obejct
class와 같은 파일, 같은 scope안에 정의 되어있음!
그래서 companion(짝궁)입니다~:)
역할은?
Factory Method 제공
ex) def props(..), def apply(...)
Class 외부에서 접근 가능한 헬퍼 함수/값 정의
: 클래스 내부는 객체를 생성해야 접근 가능하지만 object는 싱글톤이라 어디서든 접근가능!
Class에서 private로 선언한 것들에 접근 가능
: companion object와 class는 서로 private 멤버에 접근 가능!
object DemoActor {

  /**
   * Create Props for an actor of this type.
   *
   * @param magicNumber The magic number to be passed to this actor’s constructor.
   * @return a Props for creating this actor, which can then be further configured
   *         (e.g. calling `.withDispatcher()` on it)
   */
  def props(magicNumber: Int): Props = Props(new DemoActor(magicNumber))
}

class DemoActor(magicNumber: Int) extends Actor {
  def receive = {
    case x: Int => sender() ! (x + magicNumber)
  }
}

@nowarn("msg=never used") // sample snippets
class SomeOtherActor extends Actor {
  // Props(new DemoActor(42)) would not be safe
  context.actorOf(DemoActor.props(42), "demo")
  // ...
}

Actor를 만들 때는 직접 new로 만들지 말고 해당 Actor의 companion object에 있는 props()메소드를 통해 Props를 만들고
그걸로 생성하면 안전하고 표준적인 Akka 스타일 입니다. 

방식 설명 안전성
Props(new Actor(...)) by-name 인자 방식 (클로저 캡처 위험 있음) 위험
Props(classOf[Actor], ...) 리플렉션 기반, 생성자 안전성 체크 안전
Actor.props(...) 컴패니언 오브젝트 통해 분리 가장 권장

 

3.3 Creating Actors with Props

Actor는 Props 인스턴스를 actorOf 팩토리 메서드에 넘겨서 생성합니다.
이 actorOf 메서드는 ActorSystem과 ActorContext에서 제공됩니다.

구분 역할 누가 만드는가?
props(...) Props 인스턴스를 만드는 팩토리 메소드 / 설계도 제작 직접 만든다
actorOf(...) Props를 넘겨서 Actor 인스턴스를 실제로 생성하는 팩토리 메소드 / 설계도를 가지고 실제 Actor 생성 Akka가 제공함
import akka.actor.ActorSystem

// ActorSystem is a heavy object: create only one per application
val system = ActorSystem("mySystem")
val myActor = system.actorOf(Props[MyActor](), "myactor2")

ActorSystem을 사용해서 Actor를 생성하면, ActorSystem이 제공하는
guardian(보호자) Actor의 감독을 받는 최상위(top-level) Actor가 만들어집니다.
반면에, 다른 Actor의 context를 사용해서 생성하면, 그 Actor의 자식(child) Actor가 만들어집니다.

class FirstActor extends Actor {
  val child = context.actorOf(Props[MyActor](), name = "myChild")
  def receive = {
    case x => sender() ! x
  }
}

애플리케이션의 논리적인 장애 처리 구조에 맞도록 자식 → 손자 → 그 이후까지 계층 구조를 구성하는 것이 권장됩니다.

자세한 내용은 Actor Systems 항목을 참고하세요.
actorOf를 호출하면 ActorRef 인스턴스를 반환합니다.
이 객체는 해당 액터 인스턴스를 참조하는 핸들(handle)이며, 액터와 상호작용할 수 있는 유일한 방법입니다.
ActorRef는 불변(immutable)이며, 해당 액터와 1:1 관계를 갖습니다.
또한 ActorRef직렬화 가능하며, 네트워크를 인식합니다.
즉, 이 객체를 직렬화해서 네트워크를 통해 다른 호스트로 전송하더라도,
원래 노드에 존재하는 동일한 액터를 계속 참조할 수 있습니다.
actorOfname 파라미터는 선택 사항이지만, 액터에 이름을 붙이는 것이 권장됩니다.
이름은 로그 메시지나 액터 식별에 사용되기 때문입니다.
단, 이름은 비어 있으면 안 되고, $로 시작해서도 안 됩니다.
URL 인코딩된 문자(예: 공백은 %20)는 포함될 수 있습니다.
만약 같은 부모 아래에서 이미 존재하는 이름으로 액터를 생성하려 하면 InvalidActorNameException이 발생합니다.
마지막으로, 액터는 생성되면 비동기적으로 자동 시작됩니다.

3.3.1 Value classes as constructor arguments

Value Class를 생성자 인자로 사용할 때의 주의사항 입니다.
액터 props를 생성하는 데 권장되는 방식은, 런타임 시 리플렉션을 사용해서 호출할 생성자를 결정하는 방식입니다.
하지만 기술적인 제약 때문에, 해당 생성자가 Value Class를 인자로 받을 경우에는 이 방식이 지원되지 않습니다.
이런 경우에는 다음 중 하나를 선택해야 합니다:

- Value Class를 내부 값으로 “풀어서(unpack)” 넘기거나
- 직접 new를 사용해 생성자를 호출해서 Props를 생성해야 합니다

class Argument(val value: String) extends AnyVal
class ValueClassActor(arg: Argument) extends Actor {
  def receive = { case _ => () }
}

object ValueClassActor {
  def props1(arg: Argument) = Props(classOf[ValueClassActor], arg) // fails at runtime
  def props2(arg: Argument) = Props(classOf[ValueClassActor], arg.value) // ok
  def props3(arg: Argument) = Props(new ValueClassActor(arg)) // ok
}

위 소스에서 아래의 부분이 value class 입니다.

class Argument(val value: String) extends AnyVal

ValueClassActor 클래스는 생성자에서 arg를 받습니다. 이 때 들어가는 arg가 value class 입니다.
- def props1(arg: Argument) = Props(classOf[ValueClassActor], arg) // fails at runtime
첫번째 props1은 인자가 value class인데 String이기 때문에 런타임 에러가 발생 합니다.
위에서 말한것처럼 값으로 풀어서(String) 넘겨줘야 합니다.

 - def props2(arg: Argument) = Props(classOf[ValueClassActor], arg.value) // ok
두번째 props2는 arg.value가 String이라서 Props가 리플렉션 시에 생성자를 찾을수 있어서 문제가 없습니다.
즉, 값으로 풀어서 인자로 넘겨줬습니다.

- def props3(arg: Argument) = Props(new ValueClassActor(arg)) // ok
마지막으로 props3은 위에서 설명한 by-name인자 형식 입니다. 
new를 사용해서 생성자를 호출하고 Props를 생성 합니다.
* Props는 case class처럼 apply()를 가지고 있습니다. 위 그림_01 참조!
* 또한 3.2.3 Recommended Practies에서의 내용을 보면
   companion object에서 팩토리 메소드를 제공하는게 좋다고 합니다.
   만약 new연산자를 사용이 Actor내부에서의 사용이라면 위험하지만
   위 소스처럼 Actor 외부(companion object에서 사용한다면 안전 합니다.

3.3.2 DI(Dependency Injection)

Actor에 생성자 인자가 있는 경우, 그 인자들은 위에서 설명한 것처럼 Props에 함께 포함되어야 합니다.
하지만 팩토리 메소드를 반드시 사용해야 하는 경우도 있습니다.
예를 들어, 생성자 인자가 DI(의존성 주입) 프레임워크에 의해 결정되는 경우가 그렇습니다.

import akka.actor.IndirectActorProducer

class DependencyInjector(applicationContext: AnyRef, beanName: String) extends IndirectActorProducer {

  override def actorClass = classOf[Actor]
  override def produce() =
    new Echo(beanName)

  def this(beanName: String) = this("", beanName)
}

val actorRef: ActorRef =
  system.actorOf(Props(classOf[DependencyInjector], applicationContext, "hello"), "helloBean")

경고
때때로  IndirectActorProducer 에서 항상 동일한 인스턴스를 반환하도록 만들고 싶을 수도 있습니다.

예를 들어, lazy val을 사용해서 한 번 만든 Actor 인스턴스를 재사용하려는 경우가 그렇습니다.
하지만 이런 방식은 지원되지 않습니다,
왜냐하면 이건 Actor의 재시작이 어떤 의미인지에 어긋나기 때문입니다.
(관련 설명은 “What Restarting Means” 문서를 참고하세요.)

또한 DI 프레임워크를 사용할 때는, Actor Bean이 절대로 Singleton 범위를 가져서는 안 됩니다.
이유는 Actor가 Supervision전략에 따라 재시작 될수 있기 때문 입니다.
재시작이되면 객체를 다시 생성해야하는데 Singleton이면 같은 객체를 재사용하게 되는거라 상태가 꼬일수 있습니다.
여기에서 재시작이란?
  - 예외가나서 다시 생성(restart) 되는 구조를 말 합니다.
  - 뒷부분에 나올 SupervisorStrategy 대해서 더 자세하게 알아봅니다.

4. Actor API

Actor 트레이트는 단 하나의 추상 메소드만 정의합니다.
그게 바로 위에서 언급한 receive 메소드이며, 이 메소드는 액터의 동작(behavior)을 구현하는 역할을 합니다.
현재 액터의 동작이 받은 메시지와 일치하지 않을 경우, 즉 receive에서 해당 메시지를 처리하지 못하면
Akka는 자동으로 unhandled 메소드를 호출합니다.
기본적으로 이 unhandled는 akka.actor.UnhandledMessage(message, sender, recipient) 이벤트를
액터 시스템의 이벤트 스트림(event stream)에 발행합니다.

그리고 설정에서 akka.actor.debug.unhandled를 on으로 켜두면
이 메시지들은 Debug 로그로 출력됩니다.

또한 Akka Actor는 다음과 같은 기능들을 제공합니다:

  • self 참조: 현재 액터 자신의 ActorRef를 참조합니다.
  • sender 참조: 마지막으로 받은 메시지를 보낸 액터의 ActorRef입니다.
    → 보통 응답할 때 sender() ! response 식으로 사용됩니다. (Actor.Reply 항목 참고)
  • supervisorStrategy: 자식 액터들을 어떻게 감독할지 정의하는 전략으로 사용자가 오버라이드할 수 있습니다.

이 전략(supervisorStrategy)은 일반적으로 액터 내부에서 선언됩니다.
왜냐하면 decider 함수 안에서 액터의 내부 상태에 접근할 수 있어야 하기 때문입니다.
실패(failure)는 메시지 형태로 상위 액터(supervisor)에게 전달되고,
다른 메시지들과 마찬가지로 처리되지만, 일반적인 behavior(receive) 바깥에서 처리된다는 점만 다릅니다.
그렇기 때문에, 액터 안의 모든 값들과 변수들 그리고 sender 참조도 사용할 수 있습니다.

이때 sender는 직접 실패를 보고한 자식 액터를 가리킵니다.
만약 실패가 더 깊은 후손 액터에서 발생했다 하더라도
보고는 한 단계씩 상위 액터로 전달되며 처리됩니다.
즉,
supervisorStrategy는 보통 액터 안에서 정의되며,
그 이유는 실패 보고 시점에 액터의 내부 상태와 sender 정보를 활용할 수 있기 때문입니다.


context현재 액터와 현재 메시지에 대한 컨텍스트 정보를 제공합니다.
다음과 같은 기능들이 포함됩니다:

  • 자식 액터를 생성하는 팩토리 메소드 (actorOf)
  • 현재 액터가 속한 ActorSystem
  • 부모 액터(슈퍼바이저)에 대한 참조
  • 현재 액터가 감독 중인 자식 액터들
  • 라이프사이클 감시 기능 (예: watch, unwatch)
  • 액터의 동작을 교체(hotswap)할 수 있는 behavior 스택 ( Actor.HotSwap 항목 참고)
context는 액터 내부에서 자식 생성, 부모·자식 참조, 상태 감시, 동작 전환 등을 다룰 수 있는 핵심 도구입니다.

context의 멤버들을 import해서 사용하면 매번 context.라는 접두어를 붙이지 않아도 됩니다.

class FirstActor extends Actor {
  import context._
  val myActor = actorOf(Props[MyActor](), name = "myactor")
  def receive = {
    case x => myActor ! x
  }
}

나머지 공개된 메소드들은 사용자가 오버라이드할 수 있는 생명주기 훅(hook) 들이며 이후에 자세히 설명됩니다.

def preStart(): Unit = ()

def postStop(): Unit = ()

def preRestart(@nowarn("msg=never used") reason: Throwable, @nowarn("msg=never used") message: Option[Any]): Unit = {
  context.children.foreach { child =>
    context.unwatch(child)
    context.stop(child)
  }
  postStop()
}

def postRestart(@nowarn("msg=never used") reason: Throwable): Unit = {
  preStart()
}

위에 보여준 구현들은 Actor  trait에서 기본으로 제공하는 기본 구현(default) 들입니다.

4.1 Actor Lifecycle

액터 시스템에서의 “경로(path)”란 실제 액터가 존재할 수 있는 “위치”를 의미합니다.
- 액터 시스템 내의 트리구조 상의 위치를 말합니다.
ex) 액터들의 경로는 /user/parent 와 /user/parent/child로 구성 됩니다.
여기에서 왜 /my-system/parent가 아니냐고 궁금할 수 있습니다.(제가 궁금했음!ㅋㅋ)
로컬 Actor의 경로는 항상 /user/블라블라로 시작 됩니다. 
즉, /user는 사용자 정의 액터의 기본 루트 디렉토리 입니다.
그럼 /my-system는 어디에 사용되느냐?
네트워크 ActorPath에서 사용됩니다.

akka://my-system@127.0.0.1:2552/user/parent

my-system은 네트워크 통신을 위한 시스템 이름입니다.(네트워크 식별용 이름, 원격 ActorPath)

val system = ActorSystem("my-system")
val parent = system.actorOf(Props[ParentActor], "parent")
val child = parent.actorOf(Props[ChildActor], "child")


처음에는 (시스템이 초기화한 액터들을 제외하면) 모든 경로는 비어 있습니다.

actorOf()가 호출되면, 넘겨진 Props에 따라 액터가 해당 경로에 배치됩니다.
이렇게 배치된 액터 인스턴스를 “인카네이션(incarnation)”이라고 부르며,
이 인카네이션은 해당 경로(path)와 UID를 통해 식별됩니다.

위에서 parent의 ActorPath는 /user/parent이고
child의 ActorPath는 /user/parent/child 이고
이 두 액터들은 내부적으로 UID를 가지고 있습니다.

인카네이션(incarnation)은 개념입니다.
형태가 없고 Actor의 라이프사이클을 의미 합니다.

용어 형태 설명
ActorPath O (문자열) 액터가 어디 있는지 위치 정보 (/user/parent)
UID O (숫자) 한 인카네이션을 유일하게 식별하는 ID
ActorRef O (객체) Path + UID → 현재 살아있는 액터를 가리키는 핸들
Incarnation X   (개념) 하나의 UID를 가진 액터의 라이프 사이클. 구체적 형태는 없음

여기서 다음 두 개념의 차이를 이해하는 것이 중요합니다:

  • restart (재시작)
  • stop 후 새로운 액터로 재생성

재시작은 단순히 Props에 정의된 Actor 인스턴스를 새로 교체하는 것이며 이때 경로(path)와 UID는 유지됩니다.
즉, 같은 위치에 “새로운 인스턴스”가 들어온 셈이지만 같은 ActorRef를 계속 사용할 수 있습니다.
이런 재시작은 부모 액터의 Supervisor 전략에 의해 처리되며 재시작이 어떤 의미인지에 대한 더 자세한 논의는 뒤에 나옵니다.

한편 액터가 정지(stop)되면 해당 인카네이션의 생명주기(lifecycle)는 종료됩니다.

이때 생명주기 훅들이 호출되며 액터를 watch하고 있던 액터들에게 종료 알림이 전송됩니다.
이후, 같은 경로(path)에 대해 actorOf()를 다시 호출하면
이름(name)은 같지만 UID가 다른 새로운 인카네이션이 생성됩니다. (즉, 같은 위치에 다른 존재가 들어온 거지)
액터는 자기 자신, 다른 액터 혹은 ActorSystem에 의해 정지될 수 있습니다. (Stopping actors 항목 참고)

참고:
Actor는 더 이상 참조되지 않는다고 해서 자동으로 종료되지 않습니다.
생성된 모든 Actor는 명시적으로 종료시켜야 합니다.
단 하나의 예외는 부모 Actor를 종료시키면 해당 부모가 만든 모든 자식 Actor들도 재귀적으로 함께 종료된다는 점입니다.
이것만이 Actor 종료와 관련된 유일한 간단한 처리입니다.

ActorRef는 항상 단순한 경로(path)만을 나타내는 것이 아니라 “경로 + UID”라는 인카네이션(incarnation)을 함께 나타냅니다.
따라서 어떤 액터가 종료되고, 같은 이름으로 새 액터가 생성되더라도 기존의 ActorRef는 새로운 액터를 가리키지 않습니다.
즉, 이전 인카네이션에 대한 참조는 더 이상 유효하지 않게 됩니다.

반면에 ActorSelection은 단순히 경로(path)를 참조하는 개념입니다.
(또는 와일드카드를 사용한 경우 여러 경로를 가리킬 수도 있습니다.)
ActorSelection은 현재 그 경로를 누가 점유하고 있는지(인카네이션)에 대해서는 전혀 알지 못합니다.

그래서 ActorSelection은 watch로 감시할 수 없습니다.
현재 경로에 존재하는 인카네이션의 ActorRef를 얻고 싶다면 ActorSelection에 Identify 메시지를 보내면,
응답으로 ActorIdentity 메시지가 돌아오며 그 안에 실제 ActorRef가 들어 있습니다.

(→ 자세한 내용은  ActorSelection 항목 참고)
또는, ActorSelection의 resolveOne 메소드를 사용해도 됩니다.
이 메소드는 해당 경로에 존재하는 ActorRef를 Future로 반환해줍니다.

요약하면 ActorSelection은 살아있는? 존재하는 Actor에 메세지를 보낼 수 있으며 존재하지 않을 경우 로그만 남깁니다.
또한 실제로 존재하는 Actoer의 ActorRef를 가져올수 있습니다.(resolveOne,
ActorIdentity 등) 

4.2 Lifecycle Monitoring aka DeathWatch

다른 액터가 종료(영구적으로 stop됨) 되었을 때 그 사실을 알림받을 수 있는 기능입니다.
(※ 단순 재시작이 아닌, 진짜로 영구적으로 정지되었을 때!)
다른 액터가 종료될 때 Terminated 메시지가 전송되는데 이걸 받기 위해선 현재 액터가 그 대상 액터에 대해 감시 등록을 해야 합니다.(watch() 메소드를 사용)
이 기능은 액터 시스템의 DeathWatch 컴포넌트가 제공하는 서비스입니다.

감시자를 등록하는것은 쉽습니다. :

import akka.actor.{ Actor, Props, Terminated }

class WatchActor extends Actor {
  val child = context.actorOf(Props.empty, "child")
  context.watch(child) // <-- this is the only call needed for registration
  var lastSender = context.system.deadLetters

  def receive = {
    case "kill" =>
      context.stop(child)
      lastSender = sender()
    case Terminated(`child`) =>
      lastSender ! "finished"
  }
}

Terminated 메시지는 감시 등록과 액터 종료가 어떤 순서로 일어났는지와는 관계없이 생성된다는 점을 기억해야 합니다.
즉, 감시 대상 액터가 이미 종료된 이후에 감시를 등록하더라도 Terminated 메시지는 여전히 감시자(watcher)에게 전달됩니다.
감시를 여러 번 등록해도 반드시 Terminated 메시지가 여러 번 생성된다는 보장은 없지만 딱 한 번만 수신된다고 보장할 수도 없습니다.

예를 들어:

  • 어떤 액터가 종료되면서 Terminated 메시지가 이미 큐에 들어갔는데
  • 그 메시지가 처리되기 전에 다시 감시 등록이 이루어지면
  • 두 번째 Terminated 메시지가 추가로 생성될 수도 있습니다.

왜냐하면, 이미 종료된 액터를 감시 대상으로 등록하면 즉시 Terminated 메시지를 생성하게 되어 있기 때문입니다.

또한, context.unwatch(target)을 호출하면 해당 액터의 생명 상태 감시 등록을 해제할 수 있습니다.
이때 중요한 점은 이미 Terminated 메시지가 큐에 들어가 있었다 하더라도 unwatch를 호출하면 이후 그 메시지는 처리되지 않습니다.

4.3 Start Hook

Actor가 시작되자마자 preStart메소드가 호출됩니다.

override def preStart(): Unit = {
  child = context.actorOf(Props[MyActor](), "child")
}

이 메소드(preStart)는 액터가 처음 생성될 때 호출됩니다.
그리고 액터가 재시작될 때는 기본 구현에서는 postRestart 메소드가 preStart를 다시 호출하게 되어 있습니다.
→ 따라서 postRestart를 오버라이드하면 preStart의 초기화 코드가 매번 호출될지
처음 한 번만 호출될지를 직접 제어할 수 있습니다.
또한, 액터 클래스의 생성자 안에 있는 초기화 코드는 액터 인스턴스가 생성될 때마다 항상 실행되며 이건 재시작 때마다 무조건 실행됩니다.

4.4 Restart Hooks

모든 액터는 감독(supervision)을 받습니다.
즉, 다른 액터와 연결되어 있고(fault handling strategy를 통해) 예외가 발생하면,
해당 액터는 재시작될 수 있습니다 (→ 자세한 건  supervision 참고).
이러한 재시작 과정에서는 아래와 같은 생명주기 훅(hook) 메소드들이 호출됩니다:

1. 이전 액터(기존 인스턴스)는 preRestart 메소드가 호출됨으로써 재시작 원인이 된 예외와 메시지를 전달받습니다.
메시지가 없는 경우도 있는데, 예를 들어:

  • 재시작이 메시지 처리 중이 아닌 상황에서 발생했거나 (None)
  • 상위 액터가 예외를 트랩하지 않는 경우
  • 상위 액터까지 연쇄적으로 재시작된 경우
  • 혹은 형제 액터의 실패로 인해 재시작된 경우

2. actorOf를 호출할 때 전달한 초기 팩토리(Props 안에 들어 있는 생성 방식)가
    재시작 시에도 동일하게 사용되어 새 액터 인스턴스를 만든다.

3. 새롭게 생성된 액터의 postRestart 메소드는 재시작을 유발한 예외와 함께 호출됩니다.
기본적으로는 preStart 메소드도 함께 호출되며 이는 일반적인 처음 시작할 때와 같은 방식으로 동작합니다.

액터가 재시작되면 실제 액터 객체만 새로 교체됩니다.
메일박스(mailbox)의 내용은 재시작으로 영향을 받지 않기 때문에,
postRestart가 끝난 뒤에는 이전처럼 메시지 처리가 다시 이어집니다.

단, 예외를 일으킨 그 메시지 자체는 다시 전달되지 않습니다.
그리고 액터가 재시작 중일 때 해당 액터로 보내지는 메시지는 평소처럼 메일박스에 큐잉되고
액터가 재시작을 마치면 순차적으로 처리됩니다.


주의:
실패 알림(failure notification)과 일반 메시지(user message)의 처리 순서는 결정적이지 않습니다.
특히, 자식 액터가 실패하기 전에 부모에게 보낸 마지막 메시지들이 자식의 재시작 처리보다 나중에 처리될 수도 있다는 뜻입니다.
자세한 내용은 Discussion: Message Ordering 항목을 참고하세요.

4.5 Stop Hook

액터가 정지(stop) 되면 그 액터의 postStop 훅(hook)이 호출됩니다.
이 메소드는 예를 들어,
다른 서비스에서 이 액터를 등록 해제(deregister) 하는 데 사용할 수 있습니다.
그리고 이 훅은 메시지 큐잉이 이 액터에 대해 비활성화된 이후에 실행된다는 것이 보장됩니다.
즉, 이 액터가 정지된 이후에 보내지는 메시지는 ActorSystem의 deadLetters로 전달됩니다. (더 이상 이 액터가 받지 않음)

to be continue...

 

 

 

반응형

'Data Platform > Akka' 카테고리의 다른 글

Akka HelloWorld(Akka HTTP + Akka Actor연동)  (1) 2025.04.15
Akka에서 Actor 생성 시 금지하는 방식  (0) 2025.03.27
Akka Classic 뽀개기_Overview  (0) 2025.03.26
Akka Classic  (0) 2025.03.25
Akka HelloWorld  (0) 2025.03.24