2015년 8월 16일 일요일

JProcess - Erlang Process for Java.


얼마 전 Erlang 입문서를 하나 읽었다. Erlang은 함수형 언어와 Process 라는 비동기 프로그래밍 모델을 결합하여 완벽한 멀티쓰레딩/분산 컴퓨팅 환경을 제공한다.
단 몇줄의 코드로 리모트 서버에서 실행 중인 서비스를 새로운 코드로 대체하는 예제는 가히 환상적이었다.
Erlang Process 처럼 완벽한 멀티쓰레딩/분산 컴퓨팅 기술을 자바에서는 사용할 수 없는 것일까?
다행히도 가능하다는 결론은 얻었다. 게다가 그 구현 방법도 매우 단순하다. '단순'하단 것이 쉽고 간편하다는 뜻은 아니다. 물론 언어를 변경하지 않는 한 소스 코딩 단계에서 위 기능을 만족시킬 수 없다. 다만, Runtime 에 Erlang 프로세스의 쓰레드 안정성을 구현하는 것은 가능하다. 그것이 실용적인가에 고민은 뒤로 미루고 먼저 코드를 살펴보자.


interface JProcess {
void terminate(String message);
}

class ChatRoom implements JProcess {
private List<ChatUserProxy> users;
protected ChatRoom() {}

public init(String roomName) {
/* 특정 이름을 가진 채팅방을 개설한다*/
}
public enter(ChatUserProxy user) {
/* 새로운 사용자를 추가하고 다른 사용자들에게 알림*/
}
public addMessage(ChatUserProxy user, String msg) {
/* 특정 사용자의 메시지를 다른 사용자들에게 전송*/
for (ChatUserProxy c : users) c.onMessage(user.getName(), msg);
}
public leave(ChatUserProxy user) {
/* 특정 사용자가 삭제하고 다른 사용자들에게 알림*/
}
public terminate(String message) {
/* 채팅방 서비스가 종료 되었음을 모든 사용자들에게 알린다. */
}
}

@Immutable class ChatUserProxy extends JProcessProxy {
private final String name;
ChatUserProxy () {
super(CharUser.class);
}
public String getName() {
return name;
public onMessage(String userName, String msg) {
super.send("onMessage", userName, msg);
}
}

JProcess 함수(=JProcess 를 상속한 클래스의 함수)는 아래와 같은 특별한 제약조건을 가진다.
첫째, JProcess 함수 수행시 글로벌 변수(static 변수)를 참조할 수 없다.
단, Integer, String 등 불변자료형에 대한 final 상수는 예외로 한다.
둘째, JProcess 함수는 반드시 메시지 큐를 통해 비동기 메시지 Callback 형태로 함수 호출이 이뤄져야 한다.
세째. JProcess 함수 인자 전달 시, 다른 JProcess 또는 다른 Thread에서 변경 가능한 객체를 사용할 수 없다.

위의 세가지 제약을 통해 JProcess 함수 내에서 참조 가능한 모든 객체는 해당 객체 내에서만 참조 가능한 고유한 것들로 한정된다. 즉, 쓰레드간 공유 자체가 불가능하므로 쓰레드 안정성 문제가 전혀 발생하지 않게 되는 것이다.

위 제약 조건은 다음의 과정을 통해 구현된다.

첫째, JProcess 함수 수행시 글로벌 변수(static 변수)를 참조할 수 없다. 위 제약 조건을 강제하기 위해선, Java bytecode interpreter를 일부 변경하는 것이 불가피하다. Bytecode 중 putstatic, getstatic 명령어 수행시, 현재 쓰레드가 특정 쓰레드 그룹(=jprocess-thread-pool)에 속한 경우엔 Exception을 발생시키도록 변경해야 한다. 다행히 이러한 변경이 크게 어렵지 않고, JVM 호환성을 깨뜨리지도 않는다. 즉, 안정성이 검증되고 난 후엔 동일 코드를 일반 JVM 코드 상에서 실행하는 것이 가능하다.

둘째, 세쨰 조건은 자바 코드와 일부 JNI 함수를 이용해서 구현 가능하다.
이것은 아래의 JProcessProxy와 JMesageQueue를 통해서 이루어진다. 코드를 살펴보자.

abstract class JProcessProxy {
private final JMessageQueue msgQ;
public JProcessProxy(Class processClass) {
msgQ= new AsyncMessageQueue ((JProcess)craeatProcess(processClass));
}
public JProcessProxy(InetAddress remoteAddress) {
msgQ = new RemoteMessaeQueue(remoteAddress);
}
protected void send(String command, Object args...) {
for (int i = 0; i < args.length; i ++) {
args[i] = deepClone(args[i]);
}
msgQ.send(command, args);
}
public terminate(String message) {
send("terminate");
}
private static native Object deepClone(Object arg);
private static native Object craeatProcess(Class processClass);
}

class ChatRoomProxy extends JProcessProxy {
public init(String roomName) {
super.send("init", roomName);
}
public enter(CharUser user) {
super.send("enter", user);
}
public addMessage(CharUser user, String msg) {
super.send("addMessage", msg);
}
public leave(CharUser user) {
super.send("leave", user);
}
}

class ChatUser implements JProcess {
private InpuStream in; OutputStream out;
....
public void onMessage(String name, String msg);
}

둘째, JProcess 함수는 반드시 메시지 큐를 통해 비동기 메시지 Callback 형태로 함수 호출이 이뤄져야 한다.
먼저 위의 코드 중 ChatRoom의 생성자가 protected임에 주목하자. 즉 동일 패키지 외부에서는 아예 객체 생성이 불가능하도록 하였다. 다만 위의 제약조건을 강제할 수 있는 특정한 조건에서만 객체가 생성된다.
위의 JProcessProxy 는 JNI를 통해 ChatRoom 객체를 생성한 후 이를 철저히 내부적으로 감춘다. (JNI를 이용하면 public 생성자가 아니어도 호출이 가능하다.) 즉, ChatRoom 객체의 함수는 MessageQueue 를 통해 간접적으로 호출되는 것만을 허용함으로써 2번째 조건을 강제한다.

세째. JProcess 함수 인자 전달 시, 다른 JProcess 또는 다른 Thread에서 변경 가능한 객체를 사용할 수 없다.
JMessageQueue 는 특정 ThreadPool 과 연동하여, 저장된 메시지에 해당하는 특정 JProcess 객제의 함수를 호출하거나, RMI 등 직렬화를 써서 컴퓨터 내의 다른 프로세스(=OS의 프로세스)나 외부 서버에 매시지를 전달하는 역활을 담당한다.
JMessageQueue 에 메시지 인자들이 저장되기 직전에 세번째 조건을 만족시켜 주어야 한다. (참고로, 위의 예제코드와 달리 RMI를 이용할 때는 deepClone 과정을 생략할 수 있다.)

deepClone 함수는 인자로 받은 객체를 특정 JProcess 내에서만 참조 가능하도록 새로이 생성하는 함수이다. 자료 불변형 객체는 그 값을 그대로 반환하면 되지만, 그 외의 경우엔 내부 데이터의 트리 구조를 리커시브하게 모두 복사해 주어야 한다.

불변 자료형은 별도 변환없이 자유로히 공유되고, JProcess 함수 인자로 사용될 수 있다.
아래의 4조건을 만족하는 클래스의 객체는 불변자료형이며, @JImmutable 어노테이션을 추가하여 명시될 수 있다. (실제로 아래 조건을 충족하는 가는 해당 클래스의 bytecode 를 검사하여 정합성 여부를 확인할 수 있다.)
1) 멤버 필드가 없거나 모든 멤버 필드는 private 또는 final 속성을 가진다.
2) 멤버 필드의 유형은 prtimitive 또는 불변자료형이다.
3) @JNonSharedStream어노테이션으로 명시되지 않은 클래스인 경우, Constructor 함수 내에서만 멤머필드의 값을 변경할 수 있다.
4) 상위 클래스가 불변자료형이거나, @JNonSharedStream 형이다.

어쩔 수 없는 타협 @JSharableReference
@JSharableReference은 InputStream, OutputStream 등 객체 복제를 통해서 쓰레드 공유 문제를 해결할 수 없는 자료형들을 예외처리하기 위한 것이다. 복제해봐야 의미없기 때문에 @JSharableReference유형 또한 복제를 하지 않고 해당 객체를 그대로 인자로 사용한다.
일반적으로 InputStream, OutputStram 등의 객체를 여러 쓰레드가 공유하여 사용하는 일은 드물다. 애초에 쓰레드 안정성을 보장할 수 없는 구조를 가지고 있기 때문이다. Erlang 처럼 각각의 스트림을 별도의 프로세스로 분리하는 것이 바람직하나, 이는 기존의 Java 프로그램의 구조 자체를 크게 변경해야 하는 엄청난 규모의 일이 될 수도 있다. 이에 일종의 back-door를 추가하여 기존 코드와의 결합을 용이하게 하고자 하였다.
@JSharableReference 유형 객체의 안전성은 철저히 프로그래머가 책임져야 한다. 쓰레드 안정성과 관계된 모든 이슈는 해당 클래스 내에서 완벽하게 해결되어야 한다. 물론 이것이 최선인가에 대해선 계속 고민하고 있다.

결론, JProcess 는 실용적일까?
이로써 JProcess 에 대한 설계 아이디어에 대한 설명은 끝이 났다. 나머지 구현 문제는 일반적인 구현 방식을 사용하는 것이므로 생략하였다.
JProcess 함수 인자로 큰 크기의 어레이나 트리 구조를 가진 대형 객체를 인자로 전달하는 금물이다. JProcess API 설계 시 불변자료형만을을 사용하지 않는 한 기존 멀티쓰레드 구조에 비해 객체 복사 시간 및 메모리 이용 효율성 문제가 발생하는 것을 피할 수 없다. 특히 글로벌 변수 참조가 불가능한 것도 설계 시 큰 제약을 만들어낸다.

이에 @JSharableReference유형을 허용하는 것은 실용적인 타협책이 될 수는 있으나, 완벽한 Thread Safety 보장을 일부분 포기해야만 한다. 다만, 모든 쓰레드 안정성 문제를 @JSharableReference유형의 함수 내로 한정하여 명시적으로 관리할 수 있다는 장점이 있다. @JSharableReference객체를 통하지 않고는 객체 공유가 불가능하기 때문이다.

즉, 적어도 쓰레드 안정성 보장이 전혀 없는 기존의 상황보다는 큰 개선이 이뤄질 것으로 보인다. 특히 여러 프로그래머가 협업하는 환경에서 쓰레드 안정성과 관련된 소스 코드 영역을 명확히 구분하여 코딩할 수 있다는 것은 상당한 도움이 될 것으로 여겨진다.

구현된 소스코드부터 공유하는 것이 바람직한 미덕이겠으나, 당장은 필자의 생업과 연결되는 부분이 없다보니 짬을 내지 못하고 차일피일 미루게 되어, 일단 설계 아이디어부터 공유해 본다. 설계의 오점에 대한 지적이나 개선안은 고맙게 받겠다. 이왕이면 Erlang을 흉내내는 것 이상의 기능을 얹으면 좋겠다는 욕심도 있으나, 무엇보다 @JSharableReference과 관련한 부분에 대해서는 아직도 확신이 서질 않아 선뜻 구현에 착수하지 못하고 있다. 누군가 더 좋은 대안을 제안해 준다면 진심으로 고맙겠다. 
저의 게으름을 참지 못하는 분들은 따로 구현을 시작해 보셔도 좋다. 되도록 소스 코드는 일부나마 공유 좀 해주시고. ^^;;