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 언어를 대체하기 원한다면, 굳이 배타적 장벽을 가질 필요는 없을 것이다.
그들이 조금만 양보하길 바란다. ^^