본문 바로가기
기술자료/C C++

[賢彬] C로 C++/C#/JAVA처럼 OOP를 할 수 있나요?

by 알 수 없는 사용자 2009. 8. 20.
728x90
반응형

C++/자바/C#과 달리 C는 언어 차원에서 OOP 기능을 제공하지 않기 때문에 OOP를 구현하려면 상당한 애로 사항이 있다. 하지만 실제로는 여러 가지 테크닉과 꼼수를 동원해서 C로도 OOP를 많이 하고 있다. 문제는 "할 수 있는" 것과 "하기 편한" 것 사이의 넘을 수 없는 4차원의 벽인데...오늘은 같은 프로그램을 C로 짠 것과 C#으로 짠 것을 비교해 보면서 그 벽을 느껴보도록 하자. :-)

살펴볼 것은 오픈 소스계에서 가장 유명한 GUI 툴킷 중 하나인 GTK+이다. GTK+은 기본 코드가 C로 작성되어 있고, 바인딩이라고 해서 GTK+를 다른 언어에서 쓸 수 있도록 이음새 역할을 하는 라이브러리가 C++/자바/C#/파이썬 등의 주요 언어별로 하나씩 제공된다. C로 만들었음에도 불구하고 OOP를 구현하기 위해 혼신의(?) 노력을 기울인 흔적이 역력하기에 연구 대상으로는 안성맞춤이다.

Hello GTK+

설명을 위해 GTK+로 간단한 프로그램을 작성해 보았다(제일 간단한 편인데도 뭔가 뻑뻑한 느낌이 든다):

#include <gtk/gtk.h>

 

static void destroy(GtkWidget *window, gpointer data);

 

int main()

{

    GtkWidget *window;

    GtkWidget *button;

 

    gtk_init(NULL, NULL);

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(GTK_WINDOW(window), "Hello GTK+");

    gtk_container_set_border_width(GTK_CONTAINER(window), 25);

    gtk_widget_set_size_request(window, 200, 100);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);

 

    button = gtk_button_new_with_mnemonic("_Close");

    g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(destroy), NULL);

 

    gtk_container_add(GTK_CONTAINER(window), button);

    gtk_widget_show_all(window);

 

    gtk_main();

    return 0;

}

 

static void destroy(GtkWidget *window, gpointer data)

{

    gtk_main_quit();

}

이 프로그램을 리눅스 등에서

$ cc `pkg-config --cflags --libs gtk+-2.0` hellogtkplus.c -o hellogtkplus

처럼 컴파일하면 현재 디렉터리에 hellogtkplus라는 실행 파일이 생긴다(에러가 난다면 GTK+ 개발 관련 패키지가 설치되어 있지 않은 것임). 이것을 실행해 보면

처럼 화면에 표시될 것이다. 우상단의 X 버튼을 누르거나 중앙의 "Close" 버튼을 누르면 프로그램이 종료된다. 버튼에 단축키가 지정되어 있어서 Alt+C를 눌러도 버튼을 누르는 것과 같은 효과가 난다.

GTK+ 프로그램의 뼈대

GTK+ 프로그램은 기본적으로 다음과 같은 뼈대로 이루어진다:

#include <gtk/gtk.h>

 

int main(int argc, char *argv[])

{

    gtk_init(&argc, &argv);

        .

        .

        .

    gtk_main();

    return some error code;

}

원래는 여기서처럼 main() 함수의 argc, argv를 gtk_init()에 넘겨주어야 하지만, 위의 "Hello GTK+" 프로그램은 커맨드 라인 처리를 하지 않기 때문에 편의상 NULL, NULL을 넘겨주었다. 창이나 버튼, 레이블 등의 각종 위짓(윈도에서의 컨트롤에 해당)을 생성하고 처리하는 부분은 gtk_init() ... gtk_main() 사이에 들어가게 된다.

창 만들기

    GtkWidget *window;

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(GTK_WINDOW(window), "Hello GTK+");

    gtk_container_set_border_width(GTK_CONTAINER(window), 25);

    gtk_widget_set_size_request(window, 200, 100);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);

GTK+에서 창은 gtk_window_new()로 생성한 다음 gtk_window_set_title()로 제목을 설정하고, gtk_widget_set_size_request()로 크기 설정, g_signal_connect()를 써서 destroy 시그널과 해당 콜백 함수를 연결해 주면 기본 동작이 가능해진다. 이 시그널 연결 과정이 아주 중요한데, 이걸 생략하면 메인 창을 닫아도(즉 destroy 시그널이 발생해도) 프로그램이 종료되지 않는다.

위 코드에서 눈여겨 봐야 할 점은 window란 변수가 gtk_window_new()에 의해 GtkWindow 타입의 인스턴스가 되지만 타입 자체는 GtkWidget으로 선언되어 있다는 점이다. C는 OOP의 상속 개념이 없기 때문에 GTK+에서는 모든 위짓을 상위 타입인 GtkWidget으로 선언한 다음 필요할 때마다 실제 타입으로 캐스트하는 꼼수를 쓰고 있다. 그래서 다른 OOP 언어에서라면 애시당초 필요하지 않을 캐스트 연산이 거의 도배에 가까울 정도로 많이 필요하다.

그런데 여기서 생길 수 있는 의문은

    void *window;

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(window, "Hello GTK+");

    gtk_container_set_border_width(window, 25);

    gtk_widget_set_size_request(window, 200, 100);

    g_signal_connect(window, "destroy", destroy, NULL);

처럼 void * 타입을 쓰면 캐스트 없이 훨씬 깔끔하게 되지 않을까? 이 경우 코드 자체는 깔끔해지지만 타입 체크가 무력화되는 심각한 결점이 있다. 또는

    GtkWindow *window;

 

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    gtk_window_set_title(window, "Hello GTK+");

    gtk_window_set_border_width(window, 25);

    gtk_window_set_size_request(window, 200, 100);

    gtk_window_signal_connect(window, "destroy", destroy, NULL);

처럼 각 타입별로 set_title(), set_border_width(), set_size_request() 등의 온갖 함수를 일일히 구현해 줌으로써 타입 안정성을 유지하는 방법도 있을 수 있다. 그렇지만 이 방법은 OOP의 핵심 목적인 코드 재사용을 거의 포기한다는 면에서 void *의 사용 못지 않게 나쁜 해결책이다.

이런저런 문제를 고려해 볼 때 그나마 GTK+ 방식이 제일 나아 보인다.

버튼 만들기와 화면 표시

버튼도 창처럼 GtkWidget 타입으로 선언한다:

    GtkWidget *button;

 

    button = gtk_button_new_with_mnemonic("_Close");

    g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(destroy), NULL);

 

    gtk_container_add(GTK_CONTAINER(window), button);

    gtk_widget_show_all(window);

gtk_button_new_with_mnemonic()라는 길다란 이름의 함수를 써서 버튼을 생성하고 있는데, 함수 오버로딩(같은 이름의 함수를 여러 개 정의하는 것)이 안되다 보니 GTK+에는 이런 식으로 길고 복잡한 이름을 가진 함수들이 많다. 창과 마찬가지로 버튼도 g_signal_connect()를 써서 마우스를 클릭했을 때 destroy() 콜백 함수를 호출하도록 설정하고 있다.

gtk_container_add()를 써서 버튼을 창 안에 집어 넣고, gtk_widget_show_all()을 써서 최종적으로 버튼 달린 창을 화면에 표시하게 된다. 직관적으로 생각할 때는 창에 버튼을 바로 넣으면 편할 텐데, GTK+에서는 일단 창을 컨테이너로 바꾼 다음 그 안에 버튼을 넣어야 하는 번거로움이 있다.

Hello Gtk#

같은 일을 하는 프로그램을 C#으로 작성해 보았다:

using Gtk;

using System;

 

class HelloGtkSharp

{

    public static void Main()

    {

        Application.Init();

 

        Window window = new Window("Hello Gtk#");

        window.BorderWidth = 25;

        window.DefaultWidth = 200;

        window.DefaultHeight = 100;

        window.DeleteEvent += Destroy;

 

        Button button = new Button("_Close");

        button.Clicked += Destroy;

 

        window.Add(button);

        window.ShowAll();

 

        Application.Run();

    }

 

    private static void Destroy(object obj, EventArgs e)

    {

        Application.Quit();

    }

}

컴파일하는 방법은:

$ gmcs hellogtksharp.cs -pkg:gtk-sharp-2.0

여기서 쓴 API는 GTK+의 C# 바인딩인 Gtk#이라는 툴킷이다. 보이는 바와 같이 GTK+의 온갖 지저분한 캐스트 연산자와 장황한 함수명이 싹 사라지고 코드가 아주 깔끔해졌다. 그와 함께 타입 안정성이 보장되기 때문에 서로 다른 타입의 변수간 대입을 한다든지, 잘못된 타입을 메쏘드에 넘겨준다든지 하면 컴파일러가 즉각 실수를 잡아내준다.

크기는 hellogtkplus.c가 805바이트, hellogtksharp.cs가 576바이트다. 공백을 제외하고 문자끼리 비교해 보면 hellogtkplus.c는 668바이트, hellogtksharp.cs는 397바이트로 차이가 더 크게 벌어진다.

C로 OOP를 할 수 밖에 없는 경우

위에서 본 것처럼 다른 언어로 할 수 있는데도 굳이 C로 OOP를 하는 것은 엄청난 닭질이다. ㅡ.ㅡ

그럼에도 불구하고 C로 OOP를 해야만 하는 이유는 쓸만한 언어가 C 밖에 없는 환경, 속도가 절대적으로 중요한 환경 등등이 있겠다. 윈도, 리눅스, BSD 등 각종 OS들도 커널의 일부에 OOP 개념을 도입해서 만들고 있는데, 이런 환경에서는 C 말고는 사실상 선택의 여지가 없다고 하겠다.

[ 결론은 초 뻘짓 ]
728x90