Actor Model 패턴에 대한 설명은 몇몇 블로그에 나와 있으나, Java를 통해 구현한 예제는 찾기 어려워서 한번 작성해 보았다.
Actor Model 패턴에 대한 일반적인 설명은 다음과 같다.
- Actor Model 패턴에서 Actor 개념과 객체지향 언어에서의 객체 개념은 상당히 유사하다. 단지 다른 점이 있다면 객체지향에서의 메소드 호출은 메소드가 모두 실행될 때까지 기다려야 하는 동기식이다. 반면 Actor Model 패턴에서의 다른 Actor에 대한 메시지 전송은 기본적으로 비동기식이다.
- 이 비동기식 메시지 전송을 지원하기 위해서는 Actor들이 모두 Active 객체, 즉 쓰레드 기반으로 동작하는 객체여야 하고, 메시지의 전송/수신에 대한 동기화 관리가 이루어 져야 한다.
- 동기화 부분은 Actor 내부에 있는 Mailbox라는 객체를 통해서 해결되기 때문에 Actor들을 구현하는데 있어서는 동기화에 대한 고려를 전혀 하지 않아도 된다.
- akka 프레임워크가 대표적이며, Go 언어에도 시범적으로 구현된 바가 있다.
Actor Model의 단점
- 모든 Actor가 개별적으로 쓰레드를 기반으로 동작하므로 동작이 느리다.(Actor 내부에 쓰레드를 넣지 않고 구현할 수도 있겠으나 일단 이는 논의 밖이다.)
- Actor 간 주고 받는 메시지의 종류에 따라 메시지의 종류도 늘어나게 된다. 단순히 API만을 만들면 되는 것에 비하면 조금은 번거롭고, 메시지들에 대한 하위 타입 컨버팅이 필요하다.
Actor Model 패턴 클래스 다이어그램
전체적인 흐름을 설명하자면 다음과 같다.
일단 메시지를 주고 받는 주체인 IActor가 있다. IActor는 인터페이스이고, Actor 클래스는 이를 구현한 추상 클래스이다. 이 클래스 내부에서 대부분의 메시지 전달 및 수신에 필요한 동작을 수행한다. 그리고 각 개별 Actor들이 다른 동작을 할 수 있도록 processMail() 메소드와 doWork() 메소드를 추상 메소드로 제공한다.(Template Method 패턴)
Actor들이 서로간에 메시지를 전송하기 위해서는 두가지 요소가 필요하다. 우선 메시지를 받아 저장하고, Actor가 메시지를 처리할 때 저장하고 있던 메시지를 보내 주는 역할을 하는 Mailbox가 필요하다. 특히 Mailbox는 모든 동시성 문제를 처리하는 역할을 한다. 그리고 메시지를 담아 전달될 Mail 객체가 필요하다. Mail 객체 내에 실제 전달할 내용(content)이 들어가게 된다.
소스가 좀 길기 때문에 부분으로 나누어 설명하도록 하겠다.
IActor - Actor - TempControlActor/ThermostatActor/DatabaseActor
interface IActor extends Runnable{
public void tell(IMail mail);
}
abstract class Actor implements IActor{
private IMailbox mailbox = new Mailbox();
@Override
public void run() {
while(true){
receiveMail();
doWork();
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void receiveMail(){
while(mailbox.hasNext()){
IMail mail = mailbox.next();
processMail(mail);
}
}
@Override
public void tell(IMail mail){
mailbox.receiveMail(mail);
}
abstract protected void processMail(IMail mail);
abstract protected void doWork();
}
class TempControlActor extends Actor{
IActor db = new DatabaseActor();
IActor thermostat = new ThermostatActor();
public TempControlActor(){
db = ActorFactory.create(DatabaseActor.class);
thermostat = ActorFactory.create(ThermostatActor.class);
}
protected void processMail(IMail mail){
db.tell(mail);
thermostat.tell(mail);
}
protected void doWork(){
/* do nothing */
}
}
class DatabaseActor extends Actor{
protected void processMail(IMail mail){
System.out.println("db:" + mail.getContent());
}
protected void doWork(){
/* do nothing */
}
}
class ThermostatActor extends Actor{
protected void processMail(IMail mail){
Integer temp = (Integer)mail.getContent();
if(temp > 30) System.out.println("cooling");
else if(temp < 10) System.out.println("warming");
else System.out.println("stop");
}
protected void doWork(){
/* do nothing */
}
}
우선 IActor는 쓰레드로 구현된다. 따라서 쓰레드 구현이 가능하도록 Runnable 인터페이스를 기반으로 한다. IActor는 Actor의 인터페이스가 된다. Actor 클래스는 추상 클래스로써 쓰레드 기반 동작을 구현한 run() 메소드를 가지고 있다. 이 메소드는 무한 반복되는 메소드로써, 내부에서 메일의 처리(receiveMail())와 자기 할 일(doWork())을 처리한다. tell() 메소드는 외부로부터 메시지를 수신하는 메소드이다. 메소드를 수신하면 위임 객체인 Mailbox 객체에게 바로 전달한다.
실제 동작을 구현한 Actor 클래스는 모두 3개이다. TempControlActor가 주요 Actor로서, 외부로부터 메시지( 온도 )를 받아서 협력 Actor들인 DatabaseActor와 ThermostatActor에게 전달하는 역할을 한다.
DatabaseActor와 ThermostatActor는 받은 온도 정보 메시지들에 대해 간단한 연산(출력)을 수행하는 Actor이다.
ActorFactory
class ActorFactory{
private static Map<Class, IActor> actorMap = new HashMap<Class, IActor>();
public static IActor create(Class clazz){
IActor actor = actorMap.get(clazz);
if(actor != null) return actor;
try {
actor = (IActor)clazz.newInstance();
new Thread(actor).start();
actorMap.put(clazz, actor);
return actor;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
ActorFactory는 Actor를 생성하는 클래스이다. Actor에 대한 클래스 객체를 받아 Actor를 생성한다. 현재는 중복 생성이 안되도록 되어 있다. 각 Actor들은 모두 쓰레드로 동작해야 하기 때문에 new Thread(actor).start() 를 통해 쓰레드 메소드인 run() 메소드를 실행 시킨 후에 actor를 리턴한다.
IMailbox - Mailbox
interface IMailbox{
public void receiveMail(IMail mail);
public boolean hasNext();
public IMail next();
}
class Mailbox implements IMailbox{
private List<IMail> in = new LinkedList<IMail>();
public synchronized void receiveMail(IMail mail){
in.add(mail);
}
public synchronized boolean hasNext(){
return !in.isEmpty();
}
public synchronized IMail next(){
IMail mail = in.get(0);
in.remove(0);
return mail;
}
}
Mailbox는 IMailbox를 인터페이스로 구현된다. Mailbox는 외부와 연동되는 직접적인 부분이므로 동기화 문제에 대한 고려가 필요하다. 따라서 IMailbox 인터페이스에 해당하는 구현 메소드는 모두 synchronized 키워드를 통해 동시성 문제가 생기지 않도록 한다.
IMail - Mail
interface IMail{
public Object getContent();
}
class Mail implements IMail{
private Object content;
public Mail(Object content){
this.content = content;
}
public Object getContent(){
return content;
}
}
Mail 클래스는 IMail을 구현한 클래스이다. Mail은 생성과 함께 내부에 저장할 Object 타입의 content를 인자로 받는다. 그리고 Mail을 받은 Actor들은 getContent() 메소드를 통해 내부에 저장된 content를 꺼내서 처리하게 된다.
실행 방법
public static void main(String[] args) {
IActor tempControl = ActorFactory.create(TempControlActor.class);
Scanner scan = new Scanner(System.in);
while(true){
Integer temperature = scan.nextInt();
tempControl.tell(new Mail(temperature));
}
}
main() 메소드에서는 TempControlActor 객체를 생성한다. TempControlActor 내부에서는 DatabaseActor와 ThermostatActor를 생성하여 참조 객체로 가지고 있다.
그리고 Scanner 객체를 통해 키보드로 숫자(온도)를 입력 받아서 TempControlActor에 넣어주면 DatabaseActor와 ThermostatActor에게 전달하여 처리한다.
Actor Model 패턴을 이용하여 Actor들을 새로 구현할 경우에, 이미 동기화 문제는 Mailbox에 의해 해결된 상태이므로 아무런 동기화에 관한 고려를 하지 않아도 된다.
'5.디자인패턴' 카테고리의 다른 글
Adaptive Object Model(AOM) 패턴 및 그 구현 (0) | 2016.10.01 |
---|---|
Property List 패턴 (0) | 2016.09.24 |
Mediator 패턴 (0) | 2016.09.18 |
Facade 패턴 (0) | 2016.09.18 |
Command 패턴 (0) | 2016.09.18 |