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과 관련한 부분에 대해서는 아직도 확신이 서질 않아 선뜻 구현에 착수하지 못하고 있다. 누군가 더 좋은 대안을 제안해 준다면 진심으로 고맙겠다. 
저의 게으름을 참지 못하는 분들은 따로 구현을 시작해 보셔도 좋다. 되도록 소스 코드는 일부나마 공유 좀 해주시고. ^^;;




















2015년 7월 17일 금요일

Go 언어 설계자들은 OOP를 싫어하는가?

Go 언어 1.5 버젼은 Go 컴파일러 자체를 Go 언어로 구현하였다고 한다.
C 코드 부분을 자동 번역기를 돌려 Go 언어로 변환하는 편법을 사용한 것이기는 하나. 기존의 C 언어를 대체해 보겠다는 야심을 가진 그들에겐 기념할 만한 쾌거라 여겨진다.
이에 필자도, C 가 아닌 기존의 언어, 특히 OOP 언어를 Go 언어로 포팅/변환 가능한지 검토해보았다. 현재 함수 overloading 을 지원하지 않는 것은 일단 예외로 하고, 일반적인 클래스 상속과 함수 overriding 문제에 대해서만 살펴보았다.
관련 테스트 코드는 20여 줄에 불과하니 먼저 코드를 살펴보자

package main
import "fmt"

type Drawable interface { Draw(); }

type Shape interface { Drawable; Pos() Point; }

type Point struct { x, y int };
type Rect struct { Point; w, h int  }

func (p* Point) Draw() { fmt.Printf("%T: %+v\n", p, p) }
func (p* Point) Pos() Point { return *p }
//func (p* Rect) Draw() { fmt.Printf("%T: %+v\n", p, p) }

func testFn(pShape Shape, pDrawable1 Drawable, pDrawable2 Drawable, pRect* Rect, pPoint* Point) {
   if pDrawable1 != pDrawable2 {
      fmt.Printf("Drawable(&Rect) != Drawable(&Rect.Point)\n")
   }
   if pShape == pDrawable1 {
      fmt.Printf("Shape(&Rect) == Drawable(&Rect)\n")
      if pShape == pDrawable1.(Shape) {
         fmt.Printf("Shape(&Rect) == Drawable(&Rect).(Shape)\n")
      }
   }
   if pShape == pRect {
      fmt.Printf("Shape(&Rect) == &Rect\n")
   }
   if pDrawable2 == pPoint {
      fmt.Printf("Drawable(&Rect.Point) == &Rect.Point\n")
   }
}

func main() {
   rc := Rect{ Point{ 0, 0 }, 640, 480 };
   pShape := Shape(&rc);
   pDrawable := Drawable(&rc.Point);
   testFn(pShape, pShape, pDrawable, &rc, &rc.Point);
}

출력 결과.
Drawable(&Rect) != Drawable(&Rect.Point)
Shape(&Rect) == Drawable(&Rect)
Shape(&Rect) == Drawable(&Rect).(Shape)
Shape(&Rect) == &Rect
Drawable(&Rect.Point) == &Rect.Point

출력 결과의 의미를 하나 하나 분석해 보자.

  1. "Drawable(&Rect) != Drawable(&Rect.Point)"
    Rect 구조체에 포함된 Point 구조체 주소값과 Rect 구조체 주소값이 서로 다르단 뜻이다. (동일하다면, 변환 결과가 다를 수 없을 것이다.) 아마도 모든 Go 구조체는 구조체 앞 부분에 일종의 메타정보가 포함되어 있으며, Rect 구조체에 Point 구조체를 삽입할 때 해당 Meta 정보도 추가되었음이 분명하다.
    Java 등 일반 OOP 언어도 각 객체마다 메타정보(class 또는 vtable=rtti)를 가지고 있으나, 첫번째로 상속되는 클래스의 메타정보를 하위 클래스와 병합함으로써 Go 와 달리 상위 클래스로 캐스팅할 때 객체의 주소값이 변경되지 않는다.
    Go 언어도 가장 상단에 삽입된 구조체의 메타정보 위치를 하위 구조체와 공유하도록 설계하였다면 일종의 Single Inheritance 효과를 쉽게 얻을 수 있었을 것이다.
    차라리 C 언어라면 자료구조 상속을 손쉽게 처리할 수 있음에 반해 Go 언어의 이러한 특성은 아예 OOP 적인 자료구조 상속을 아예 불가능하게 한다는 것이 심각한 문제이다.
  2. "Shape(&Rect) equals Drawable(&Rect)"
    "Shape(&Rect) == Drawable(&Rect).(Shape)"
    특이하게도 Rect 구조체를 Shape 로 casting 한 결과가 Drawable 로 casting 결과가 동일하다. 위 소스에서 func (p* Rect) Draw() 가 코멘트처리되어 있음에도 불구하고 나온 결과이다.
    또한 Shape(pDrawable1) 대신에 pDrawble1.(Shape) 을 통해 dynamic casting 도 가능하다.
    그렇다면, interface 를 통해 vtable 이라도 쉽게 만들 수 있을까? 조금 희망이 있나?

  3. "Shape(&Rect) == &Rect"
    "Drawable(&Rect.Point) == &Rect.Point"
    필자는 Go interface 가 {vtable(=iftable)+구조체 주소값}을 가진 객체라고 예상하고 있었다. (참고로, C++ 의 경우, 다중 상속된 다른 class 로 타입 변환시 그 결과값과 그 객체의 주소값이 다를 수 있다) 그런데, 큰 착각이었다.
    위 결과를 보면 Go 구조체를 interface 로 바꾸면 단지 타입 변환만 일어날 뿐 구조체에 대한 주소값이 그대로 사용됨을 알 수 있다.
    의외로 Go interface 구현 방식은 C++ 이 아니라 자바와 유사한 방식을 택했다. 자바 또한 Object 를 interface 로 변환할 때 타입 검사만 할 뿐 별도의 구조체를 이용하지 않는다. 결국, 자바와 마찬가지로 Go inteface 도 함수 호출 시마다 해당 객체 클래스의 함수를 검색하여 찾아야만 하는 것이다. (참고로 퍼포먼스를 위해 iftable을 만들어 캐싱한다.)

결론적으로,
Go 언어에서 기존의 OOP 상속을 구현하는 것은 거의 불가능에 가깝다.
클래스 상속 시마다 function table 을 직접 만들어 관리하는 것이야 어찌 해결한다 하더라도, 자료구조 상속이 불가능하므로, 일일이 상위 구조체의 내용을 하위 클래스에 복사해 넣어야만 한다.
이건 전혀 의미가 없는 일이다. 그런 코드를 들이밀며 OOP 라고 주장한다면 너무 씁쓸한 블랙 코미디가 될 것이다.

C언어는 일종의 Mother language 다. 그 결과의 효율성을 떠나 이 세상의 모든 언어는 C 언어로 변환될 수 있기 때문이다.
그러나 현재의 Go 언어는 매우 배타적인 언어로 보여진다. 오히려 C 언어라면 쉽게 구현할 수 있는 것을 Go 언어로는 불가능하기 때문이다. 아마도 그들이 일부러 OOP 적인 개발이 불가능하도록 언어를 설계하진 않았으리라 여기며, 현재의 설계를 끝까지 고집하지 않기를 기대한다.
Go 언어는 간결하고 강력한 매력적 언어이다. 자신들의 철학과 성능을 해치는 것이 아닌 한, 특히 그들의 야심대로 C 언어를 대체하기 원한다면, 굳이 배타적 장벽을 가질 필요는 없을 것이다.
그들이 조금만 양보하길 바란다. ^^