Akka 정리 (상)

김대현·2020년 2월 4일
0

Akka 정리 (상)

#1. Akka 특징

  • 반응성 : nonblocking/asyncrhonous
  • 메시지 중심 : event-driven, 메시지 중심
  • 모듈화 : Akka 세계서의 컴포넌트는 Actor이고, 이 Actor는 서로 완벽하게 독립적이며, 코드의 응집성, 느슨한 결합, 캡슐화와 같은 프로그래밍 원리를 완벽하게 구축한다.

처리율

멀티코어를 사용하는 프로그램의 속도는 프로그램 내부에 존재하는 순차적 부분이 사용하는 시간에 의해서 제한된다. - 암달의 법칙

Thread나 Task를 만들어서 ExecutorService에게 제출하는 식으로 동시성 코드를 작성할면 여러 개의 스레드가 동시에 작업 수행 한다.

  1. but 프로그램 안에는 Thread나 Task가 포함하지 않는 코드가 존재
  2. but synchronized 블록이나 데이터베이스, 네트워크 API 호출 등을 만날 때 다른 스레드와 나란히 줄을 서서 순차적으로 작업을 수행해야 하는 경우도 있다. 이러한 순차적 코드의 또 다른 이름은 '블로킹 콜'이다

아카는 스칼라 언어로 작성되었지만 더 아래로 내려가면 자바의 동시성 패키지를 사용하기 때문에 아카를 사용하는 것은 궁극적으로 자바의 Thread나 Task를 사용하는 것과 마찬가지다.

하지만 아카를 사용하면 프로그램 곳곳에 존재하는 순차적 부분, 블로킹 콜을 전부 없애거나 적어도 최소한으로 만드는 것이 가능해진다.

아카를 이용해서 프로그램을 설계한다는 것은 블로킹 호출이 일어나는 지점을 난블로킹 호출로 전환하는 작업을 수행하는 것을 의미한다.

스케일 아웃

전통적으로 웹서버가 많은 트래픽을 감당하는 방법은 로드 밸런서 뒤에 여러 대의 서버를 설치하는 방식이다. 다만 이 방법은 각각의 톰캣들이 커뮤니케이션할 수 없는 문제, 하나의 컴포넌트만 떼어서 독립적으로 실행하기 어려운 문제가 있다.

아카의 경우라면 이야기가 다르다. 아카 세계에서 컴포넌트는 액터이며, 어느 액터 하나만 콕 집어내어 독자적인 JVM으로 실행하는 것은 거의 아무런 노력이 들지 않는다.

모듈화

액터가 제공하는 가장 강력한 기능의 하나는 모듈화다. 액터는 서로 완벽하게 독립적이며 오로지 메시지를 주고 받는 방식으로만 커뮤니케이션한다.

차세대 동시성 모델

Design Pattern - 어댑터 패턴 : 한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

액터 모델은 동시성 코드를 작성하기 위해서 다른 차원의 추상수준을 제공한다. 여기에서는 잠금장치(lock)와 같은 구조물이 없다. 액터 모델에서는 오직 액터만 존재한다. 그들은 서로 독립적인 구조물이며 서로 메서드를 호출할수도 없고 new를 통한 인스턴스를 만들 수 도 없다.


#2. Ping Pong 예제

//Main.java
public static void main(String[] args) {
  ActorSystem actorSystem = ActorSystem.create("TestSystem");
  ActorRef ping = actorSystem.actorOf(Props.create(PingActor.class), "pingActor");
  ping.tell("start", ActorRef.noSender());
}

//PingActor.java
public class PingActor extends UntypedActor {
	private LoggingAdapter log = Logging.getLogger(getContext().system(), this);
	private ActorRef pong; 
	
	@Override
	public void preStart() {
		this.pong = context().actorOf(Props.create(PongActor.class, getSelf()), "pongActor");
	}
	
	@Override
	public void onReceive(Object message) throws Exception {
		if (message instanceof String) {
        	String msg = (String)message;
        	log.info("Ping received {}", msg);
        	pong.tell("ping", getSelf());
        }
	}
}

//PongActor.java
public class PongActor extends UntypedActor {
	private LoggingAdapter log = Logging.getLogger(getContext().system(), this);
	private ActorRef ping;

	public PongActor(ActorRef ping) {
		this.ping = ping;
	}

	@Override
	public void onReceive(Object message) throws Exception {
		if (message instanceof String) {
			String msg = (String)message;
			log.info("Pong received {}", msg);
			ping.tell("pong", getSelf());
			Thread.sleep(1000);
		}
	}
	
}


  • 액터 시스템
  • 액터 만들기 (Props, 액터의 이름)

props

액터를 만들기 위한 조리법. 액터를 만드는 데 필요한 구성요소를 담는 일종의 config같은 클래스다.

액터의 이름

액터들은 트리와 비슷한 계층구조를 형성한다. A라는 액터가 B라는 액터를 만들면 A는 B의 부모가 된다. 그렇기 때문에 경로라는 개념이 있으며 액터의 이름은 이러한 경로에서 의미를 갖는다.

actorOf

액터를 만들기 위한 '팩토리' 메서드. 첫번째 인수는 액터를 위한 타입, 그 다음에 나열되는 인수는 생성자를 호출할 때 전달되는 인수를 의미한다.

ActorRef

액터시스템 내에서 사용하는 액터는 모두 ActorRef다. 액터를 가리키는 참조이며 모든 액터는 오직 ActorRef라는 타입에 의해서만 접근될 수 있다. pingActor에 아무리 많은 퍼블릭 메서드를 정의해도 우리가 가지고 있는 것은 pingActor가 아니라 ActorRef이기 때문에 퍼블릭 메서드에 직접 접근할 수 없다.

장소 투명성

사용할 액터가 물리적으로 어디에 존재하는지 알 필요가 없음을 가리키는 개념이다. 앞에서 본 것처럼 PingActor는 ActorRef에 둘러쌓여서 장소를 알 수 없으며 오직 ActorRef를 가지고 작업을 수행한다.

메시지 전송

객체지향에서는 '객체를 만든 다음 그 객체가 가지고 있는 메서드를 호출'한다. 액터 세계에서는 '액터를 만든 다음 그 액터에 메시지를 전송'하는 것이 가장 기본이다.
ActorRef는 tellask라는 두 개의 API를 제공한다.

tell

  • 메시지
  • 발신자

첫번째 인수인 메시지는 임의의 자바 객체(Object)가 될 수 있다. PingActor, PongActor 클래스를 정의할 때 'extends UntypedActor'라고 정의하였기 때문에 액터가 받아들이는 메시지의 타입이 미리 정해져 있지 않음을 의미한다.
발신자의 주소는 반드시 ActorRef 타입을 가져야 한다. 즉 발신자는 어떤 액터다.

  • getself() : 이 메서드는 액터 자신의 ActorRef 객체를 리턴한다.
  • getSender() : 포워딩
  • noSender() : 발신인 주소 null (주소가 의미없을때)

메일박스

액터에 어떤 메시지를 전송하면 그것은 액터마다 가지고 있는 메일박스에 저장됨
액터 하나를 뗴어놓고 생각해보면 Node.js의 이벤트 처리 방식과 유사하다. 전달되는 메시지가 메일박스에 차곡차곡 쌓이고, 액터는 그 메시지를 한 번에 하나씩 처리한다.

onReceive

액터시스템은 내부에 스레드풀을 보유하고 있다. 디스패처라고 부르기도 하는데, 액터시스템은 일정한 규칙에 따라 스레드를 액터에게 할당한다. 이렇게 스레드가 할당된 액터는 메시지를 처리하고 스레드를 풀로 반납한다. 액터시스템은 반납된 스레드를 다른 액터에게 할당하는 방식으로 작업을 이어나간다.

  • 메일박스에서 메시지를 하나 꺼낸다.
  • 액터의 onRecieve 메서드를 호출하면서 메시지를 인수로 전달한다.
  • onReceive의 동작이 완료된다.
  • 스레드를 반납할 시간이 되었는지를 확인한다.
  • 시간이 되었으면 스레드를 반납하고 다음 순서를 기다린다.
  • 아직 시간이 되지 않았으면 메일박스에서 다음 메시지를 꺼내고 위 과정을 반복한다.
  1. 어느 특정한 시점에서 보았을 때 액터의 onReceive 메서드를 호출하는 스레드는 반드시 1개로 국한된다. 디스패처가 하나의 엑터에 2개 또는 그 이상의 스레드를 동시에 할당하지 않기 때문이다.
  1. 하지만 액터의 onReceive 메서드를 호출하는 스레드가 영원히 똑같은 스레드인 것은 아니다. 다른 스레드로 얼마든지 달라질 수 있다.

액터 라이프사이클

PingActor는 preStart()에서 PongActor를 만들었다.

  • actorOf를 systemActor가 아니라 context()로부터 호출하고 있다.
  • Props.create() 메서드가 인수 두 개를 받아들이고 있다.

모든 액터는 자신이 실행되는 액터시스템의 환경이나 문맥, 즉 ActorContext에 접근할 수 있는 context() 메서드를 포함하고 있다. (ActorSystem의 모습을 나타내는 객체)

모든 액터는 다음과 같은 라이프사이클을 가지고 있다.

  • 생성
  • 재시작
  • 멈춤

재시작은 액터시스템의 어딘가에서 에러가 발생해 ActorRef가 내부에 저장된 액터 객체를 버리고, 동일한 객체를 클래스의 생성자를 호출함으로써 액터를 새로 만드는 경우다.

profile
FrontEnd Developer with React, TypeScript

0개의 댓글