2011년 7월 15일 금요일

GNOME 개발 설명서 / 동영상

오랜만에 GNOME 개발자 센터를 들어갔더니, 모양 뿐 아니라 내용도 확 바뀌어 있습니다. 예전에도 있었는데 제가 몰랐던 건지도 모르지만, 10분짜리 설명서(10-minute tutorials)가 가장 먼저 눈에 띄었습니다. (접속한 페이지의 실제 이름은 GNOME 개발자 플랫폼 데모입니다) 이미지 보기, 기타 튜너, 메시지 보드 등과 같은 여러 예제를 통해 단순한 GTK+ 위젯 라이브러리 사용법만 보여주는것 뿐 아니라, 말 그대로 GNOME 플랫폼의 중심이 되는 GTK+ / Clutter / GStreamer / WebKitGtk 라이브러리 등을 이용하여 유용하게 참고할 수 있는 간단한 응용 프로그램을 Anjuta 통합 개발 도구, Glade UI 편집기를 이용하여 개발하는 방법을 설명합니다. 또한 C / C++ / JavaScript / Python / Vala 등과 같은 언어별 예제도 각각 제공하고 있습니다.

최근 계속 연재되는 파이썬(Python) 언어와 GTK+ / Clutter / GStreamer 라이브러리를 이용한 GNOME 개발 동영상(screencast)도 볼만합니다. 몇몇 예제는 그놈 개발자 사이트 데모 프로그램과 겹치는 것도 있습니다.

  1. GNOME 스크린캐스트 - 01. 첫번째 GTK+ 어플리케이션 (2011-06-16): 파이썬을 이용해 기본 기능을 가진 GTK+ 프로그램 만들기

  2. GNOME 스크린캐스트 - 02. 화려한 사진 어플리케이션 만들기 (2011-06-22): 파이썬과 GTK+를 이용해 간단한 사진 프로그램 만들기

  3. GNOME 스크린캐스트 - 03. 멋진 계산기 만들기 (2011-06-29): 파이썬과 GTK+를 이용해 간단한 계산기 프로그램 만들기

  4. GNOME 스크린캐스트 - 04. 우아한 기타 튜너 만들기 (2011-07-07): 파이썬과 GTK+, GStreamer를 이용해 기타 튜너 프로그램 만들기

  5. GNOME 스크린캐스트 - 05. 매력적인 동영상 재생기 만들기 (2011-07-15): 파이썬과 GTK+, GStreamer, Clutter를 이용해 동영상 재생기 만들기


gedit 텍스트 편집기와 Glade UI 편집기만을 이용해 파이썬 언어의 간결함과 GNOME 플랫폼 라이브러리의 강력함을 잘 보여주고 있습니다. 여담이지만, 영어가 짧아 단어만 알아듣는 본인도 코드만 보고 이해할 수 있었습니다. :)

물론 이 글에서 소개한 설명서는 대부분 아마도 많은 개발자에게 GNOME 플랫폼의 우수성을 전파하고 사용을 독려하기 위해 매우 기본적인 내용만 맛보기로 소개하기 때문에 더 심각하고(?) 자세한 내용을 알고 싶다면 각 기술에 대한 심층적인 공부가 필요합니다.

또한 GNOME 플랫폼 라이브러리라는 제목을 달고 있지만 대부분의 기술이 반드시 GNOME 환경에서만 동작하는 게 아니므로 리눅스 관련 개발자라면 한 번 들여다보는 것도 좋을 것 같습니다.

(이 글은 개인 블로그에 함께 게재되어 있습니다)

2011년 7월 10일 일요일

GObject 객체 지향 프로그래밍 (4)

이전 글에 계속 이어집니다.

객체 속성 정보 얻기

EdcHost 객체의 속성 정보를 실행 중에 얻어볼까 합니다.

왜 또 갑자기 불필요한 예제를 꺼내냐고 물어보실 분이 있을 것 같아 말하자면, 가끔 요긴한 경우가 있기 때문입니다. 예를 들어 EdcHost 객체를 상속받은 EdcHostDoosan, EdcHostKia, EdcHostLitte 객체가 여러 개 존재할 경우, 이 객체들은 EdcHost의 공통 속성 뿐 아니라 본인의 속성도 따로 가집니다. 이러한 여러 객체를 관리할때, 특정 속성이 있는지 여부를 검사해서 관련 UI를 활성 / 비활성하거나, 편집 UI 자체를 속성 스펙과 목록을 이용해 100% 자동화하는 게 가능합니다. (Glade 처럼 말이죠) 물론 옵션 같은 플래그(flags) 변수를 정의하는 방법 등 여러가지 대안이 가능하겠지만, 최초 객체 설계시 고려하지 못했던 기능이나 속성을 나중에 계속 추가해 나가야 하는 경우 기존에 만든 객체를 매번 다시 수정하고 업그레이드하는 것보다 더 안전하고 깔끔한 방법이 될 수 있습니다. 그리고 당연히 더많은 응용이 있겠지만, 일단 알아두면 나중에 어떤 식으로든 도움이 되리라 생각합니다.

일단, 다음 코드는 객체가 가지고 있는 속성 이름과 각 속성의 현재 값을 출력합니다.
static void
print_properties (GObject *object)
{
GObjectClass *oclass;
GParamSpec **specs;
guint n;
guint i;

oclass = G_OBJECT_GET_CLASS (object);
specs = g_object_class_list_properties (oclass, &n);
for (i = 0; i < n; i++)
{
GParamSpec *spec;
GValue value = { 0 };
gchar *str;

spec = specs[i];

g_value_init (&value, spec->value_type);
g_object_get_property (G_OBJECT (object),
spec->name,
&value);
str = g_strdup_value_contents (&value);

g_print ("property '%s' is '%s'\n",
spec->name,
str);

g_value_unset (&value);
g_free (str);
}
g_free (specs);
}

{
EdcHost *host;

/* ... */

host = g_object_new (
EDC_TYPE_HOST,
"address", "demo.emstone.com",
"port", 8081,
NULL);
print_properties (G_OBJECT (host));
g_object_unref (host);

/* ... */
}

위 코드에서 분명하게 이해해야 하는 점은, 객체 인스턴스가 아닌 객체 클래스에게 속성 정보를 질의한다는 점입니다. 모든 속성의 스펙을 얻기 위해 g_object_class_list_properties() 함수를 사용하고, GValue 객체에 속성 값을 가져온 다음, 문자열로 출력하기 위해 g_strdup_value_contents() 함수를 이용해 변환하고 있습니다.

객체에 어떤 속성이 있는지 알아보려면 g_object_class_find_property() 함수를 이용하면 됩니다.

속성 변경 알림 시그널 이용하기

객체의 속성 값을 변경할 때 g_object_set() 함수를 이용하면 좋은 점은, 값을 변경하면 자동으로 시그널(signal)이 발생한다는 점입니다. GObject 시스템에서 시그널은 특정 사건(event)이 일어나면 발생(emit)합니다. 대부분의 경우 시그널은 객체 클래스 초기화시에 정의해야 하지만, 다행히도 속성 값이 변경될때 발생하는 시그널은 특별한 작업을 해주지 않아도 기본적으로 동작합니다. 따라서 "notify::property-name" 형식의 이름을 가지는 시그널에 콜백 함수를 연결하면 객체 값이 변경될때 자동으로 호출되는 함수를 등록할 수 있습니다.

static void
property_notified (GObject *object,
GParamSpec *pspec,
gpointer data)
{
GValue value = { 0 };
gchar *str;

g_value_init (&value, pspec->value_type);
g_object_get_property (object, pspec->name, &value);
str = g_strdup_value_contents (&value);

g_print ("property '%s' is set to '%s'\n",
pspec->name, str);

g_value_unset (&value);
g_free (str);
}

{
EdcHost *host;

host = g_object_new (EDC_TYPE_HOST, NULL);

g_signal_connect (host,
"notify::address",
G_CALLBACK (property_notified),
NULL);
g_signal_connect (host,
"notify::port",
G_CALLBACK (property_notified),
NULL);

g_object_set (host,
"address", "192.168.0.1",
"port", 8087,
NULL);

edc_host_set_address (host, "192.168.0.22");

g_object_unref (host);
}

참고로 이 기능은, 디자인 패턴에서 말하는 관찰자(observer) 패턴일 수도 있고, GObject 매뉴얼에서 사용하는 것처럼 일종의 메시징 시스템 역할도 합니다. 예를 들어 모델(model)의 값이 변경되면 자동으로 뷰(view) 역할을 하는 GUI에 반영하는 코드를 작성할 경우 기존 객체 구현 코드를 수정하지 않고, 다시 말해 의존성을 추가하지 않고 기능을 구현할 수 있게 도와주어 객체간 결합도를 없애 줍니다.

자 그런데, 위 예제에서 edc_host_set_address() 를 사용할 때는 콜백함수가 호출이 안되는 문제점이 있습니다. 왜냐하면 이 함수는 내부 address 변수를 직접 수정하기 때문에 값이 변경되었는지 여부를 GObject 시스템이 알 방법이 없기 때문입니다. 따라서 기존 코드를 수정해야 하는데, 첫번째 방법은 접근자를 이용하더라도 내부적으로 g_object_set() 을 호출하도록 하는 겁니다. (여기서는 'address' 관련 API만 보여드립니다)
void
edc_host_set_address (EdcHost *host,
const gchar *address)
{
g_return_if_fail (EDC_IS_HOST (host));
g_return_if_fail (address != NULL);

g_object_set (host,
"address", address,
NULL);
}

하지만 이 방법은 약간의 오버헤드가 있을 수 있습니다. 두번째 방법은, g_object_notify() 함수를 이용해 직접 알려주는 겁니다.
void
edc_host_set_address (EdcHost *host,
const gchar *address)
{
EdcHostPrivate *priv;

g_return_if_fail (EDC_IS_HOST (host));
g_return_if_fail (address != NULL);

priv = EDC_HOST_GET_PRIVATE (host);

g_free (priv->address);
priv->address = g_strdup (address);

g_object_notify (G_OBJECT (host), "address");
}

edc_host_set_property() 함수 안에서 중복되는 코드도 정리해 봅시다.
static void
edc_host_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
EdcHost *host = EDC_HOST (object);
EdcHostPrivate *priv;

priv = EDC_HOST_GET_PRIVATE (host);

switch (property_id)
{
case EDC_HOST_PROP_NAME:
edc_host_set_name (host, g_value_get_string (value));
break;
case EDC_HOST_PROP_ADDRESS:
edc_host_set_address (host, g_value_get_string (value));
break;
case EDC_HOST_PROP_PORT:
edc_host_set_port (host, g_value_get_int (value));
break;
case EDC_HOST_PROP_USER:
edc_host_set_user (host, g_value_get_string (value));
break;
case EDC_HOST_PROP_PASSWORD:
edc_host_set_password (host, g_value_get_string (value));
break;
default:
/* We don't have any other property... */
G_OBJECT_WARN_INVALID_PROPERTY_ID (object,
property_id,
pspec);
break;
}
}

시그널이 중복 발생할 경우를 염려할 필요는 없습니다. 시그널은 GObject 내부적으로 알아서 잘 정리되어 한 번 변경하면 한 번만 시그널이 발생합니다.

오늘은 여기까지입니다... :)

(이 글은 개인 블로그에 함께 게재되어 있습니다)

GObject 객체 지향 프로그래밍 (3)

속성 (Properties) 추가하기

이제, GObject 속성(properties) 기능을 추가하려고 하는데, 왜 쓸데없이 일을 만들어서 하냐고 물으면 할 말이 있어야할 것 같아서, GObject 속성의 특징을 요약해 봤습니다.

  1. 단일 API로 모든 속성 값을 얻어오거나 변경하기

  2. 속성 변경시 자동으로 호출되는 함수 등록하기 (시그널 이용)

  3. 실행 중에 속성에 대한 정보 얻어내기


물론 이미 많은 언어와 라이브러리가 그 이상의 기능을 지원하기도 하고, 일정 능력 이상의 개발자라면 직접 구현하는게 아주 어려운 것도 아닙니다. 하지만 이미 잘 구현되어 검증받은 라이브러리가 있는데 굳이 새로운 바퀴를 만들 필요는 없겠지요? 아무튼, 정확한 내용은 글을 적으면서 하나씩 설명해 나가겠습니다.

GObject 객체에 속성을 추가하려면 속성의 값(value)이 어떤 형(type)인지, 이름이 무엇인지, 값의 범위는 어떻게 되는지, 기본값은 무엇인지 등을 정의해서 알려주어야 합니다. (C++이나 Java에서 클래스 멤버 변수를 정의하는 것과 비슷합니다) 이러한 정보를 줄임말로 스펙(spec.)이라고 한다면, 속성을 추가한다는 건 다른 말로, 스펙으로 명시한 속성 정보를 클래스에 설치(install)하는 것을 의미합니다. 객체 인스턴스마다 속성의 실제 값(value)은 모두 다르겠지만, 어떤 속성이 있는지 그 속성은 어떻게 구성되어는지는 모두 동일하겠지요. (참고로 GObject 관련 API를 훑어보시면 정확히 모르더라도 지금 언급한 개념의 단어로 이루어진 API가 꽤 많은 걸 아시게 될 겁니다) 그렇기 때문에, 속성을 추가하는 작업은 클래스 초기화 함수에서 이루어집니다.

다음은 기존 예제에서 속성을 추가한 코드입니다. (변경된 부분만 보여드립니다)

edc-host.c
/* ...[snip]... */

enum
{
EDC_HOST_PROP_0, /* ignore */
EDC_HOST_PROP_NAME,
EDC_HOST_PROP_ADDRESS,
EDC_HOST_PROP_PORT,
EDC_HOST_PROP_USER,
EDC_HOST_PROP_PASSWORD
};

/* ...[snip]... */

static void
edc_host_get_property (GObject    *object,
guint       property_id,
GValue     *value,
GParamSpec *pspec)
{
EdcHost *host = EDC_HOST (object);
EdcHostPrivate *priv;

priv = EDC_HOST_GET_PRIVATE (host);

switch (property_id)
{
case EDC_HOST_PROP_NAME:
g_value_set_string (value, priv->name);
break;
case EDC_HOST_PROP_ADDRESS:
g_value_set_string (value, priv->address);
break;
case EDC_HOST_PROP_PORT:
g_value_set_int (value, priv->port);
break;
case EDC_HOST_PROP_USER:
g_value_set_string (value, priv->user);
break;
case EDC_HOST_PROP_PASSWORD:
g_value_set_string (value, priv->password);
break;
default:
/* We don't have any other property... */
G_OBJECT_WARN_INVALID_PROPERTY_ID (object,
 property_id,
 pspec);
break;
}
}

static void
edc_host_set_property (GObject      *object,
guint         property_id,
const GValue *value,
GParamSpec   *pspec)
{
EdcHost *host = EDC_HOST (object);
EdcHostPrivate *priv;

priv = EDC_HOST_GET_PRIVATE (host);

switch (property_id)
{
case EDC_HOST_PROP_NAME:
g_free (priv->name);
priv->name = g_value_dup_string (value);
break;
case EDC_HOST_PROP_ADDRESS:
g_free (priv->address);
priv->address = g_value_dup_string (value);
break;
case EDC_HOST_PROP_PORT:
priv->port = g_value_get_int (value);
break;
case EDC_HOST_PROP_USER:
g_free (priv->user);
priv->user = g_value_dup_string (value);
break;
case EDC_HOST_PROP_PASSWORD:
g_free (priv->password);
priv->password = g_value_dup_string (value);
break;
default:
/* We don't have any other property... */
G_OBJECT_WARN_INVALID_PROPERTY_ID (object,
 property_id,
 pspec);
break;
}
}

/* ...[snip]... */

/* class initializer */
static void
edc_host_class_init (EdcHostClass *klass)
{
GObjectClass *gobj_class;
GParamSpec *pspec;

gobj_class = G_OBJECT_CLASS (klass);
gobj_class->finalize = edc_host_finalize;
gobj_class->set_property = edc_host_set_property;
gobj_class->get_property = edc_host_get_property;

g_type_class_add_private (gobj_class,
 sizeof (EdcHostPrivate));

pspec =
g_param_spec_string ("name",               /* name */
"Name",               /* nick */
"the name of a host", /* blurb */
"",                   /* default */
G_PARAM_READWRITE);
g_object_class_install_property (gobj_class,
EDC_HOST_PROP_NAME,
pspec);

pspec = g_param_spec_string ("address",
"Address",
"the address of a host",
"",
G_PARAM_READWRITE);
g_object_class_install_property (gobj_class,
EDC_HOST_PROP_ADDRESS,
pspec);

pspec = g_param_spec_int ("port",
"Port",
"the port number of a host",
0,     /* minimum */
65535, /* maximum */
0,     /* default */
G_PARAM_READWRITE);
g_object_class_install_property (gobj_class,
EDC_HOST_PROP_PORT,
pspec);

pspec = g_param_spec_string ("user",
"User",
"password for authetication",
"",
G_PARAM_READWRITE);
g_object_class_install_property (gobj_class,
EDC_HOST_PROP_USER,
pspec);

pspec = g_param_spec_string ("password",
"Password",
"password for authetication",
"",
G_PARAM_READWRITE);
g_object_class_install_property (gobj_class,
EDC_HOST_PROP_PASSWORD,
pspec);
}

제일 먼저 정의된 열거형 타입에 대해 설명하자면, 클래스 내부에서 속성은 정수형 숫자로 관리됩니다. 예를 들어 1번 속성, 3번 속성처럼 직접 정수형을 사용해도 되지만, 관례적으로 가독성을 위해 열거형으로 정의합니다. 이렇게 정의한 번호를 클래스에 속성을 설치할때 지정하면 [g_object_class_install_property()],  edc_host_{get/set}_property() 속성 읽기 / 쓰기 함수의 인자로 `property_id'가 전달되는데, 이 ID가 바로 속성 번호입니다. 물론 속성 번호는 g_object_class_override_property() 같은 다른 API에서도 사용합니다.

edc_host_class_init() 클래스  초기화 함수를 보면, g_param_spec_*() 함수를 이용하여 각 속성의 스펙을 정의해서 g_object_class_install_property()함수를 이용해 클래스 객체에 설치합니다. 그리고,속성 읽기 /쓰기 메쏘드를 재정의합니다. 참고로 API 문서를 확인하시면, 다양한 형(type)을 위한 스펙 정의 함수가 있는 걸 알 수 있습니다. 속성 스펙을 정의할때 마지막에 넣어주는 플래그(flags)는 속성의 특성을 정의하는데, GParamFlags 설명을 한 번 읽어보시면 어렵지 않게 이해할 수 있습니다. 여기서는 모든 속성을 읽고 쓰기 가능하게 했습니다.

재정의된 edc_host_{get/set}_property() 속성 읽기 / 쓰기 메쏘드 함수를 보면, 접근자(accessor) 함수와 동일한 작업을 합니다. 다른 점이라면 속성 ID에 따라 GValue 객체에서 값을 읽거나, 값을 할당한다는 점입니다. GValue 객체는 쉽게 말해 어떤 형(type)의 값이라도 담을 수 있는 일반적인 값(generic values)입니다. 참고로 이 역시 다양한 형(type)을 위한 g_value_{set,get}_*() 형태의 함수가 존재하므로 이를 그대로 이용하면 됩니다. (물론 더 능숙하게 사용하려면 API 문서를 한 번 훑어보는게 좋겠지요)

여기까지 이해하셨다면 아시겠지만, GObject 시스템은 속성에 전반적인 틀과 관리 체계만 제공할 뿐 실제 속성을 다루는 작업은 대부분 직접 구현해야 합니다. 이는 프로그래머의 자유도를 높여 주기도 하지만, 불필요한 반복 작업을 유발하기도 합니다. 그리고 이 때문에 Vala 같은 GObject 기반 언어가 새로 만들어지기도 했습니다.

속성 (Properties) 사용하기


이렇게 정의한 속성을 객체 외부에서 사용하기 위해 몇 가지 방법이 있지만, 가장 쉽고 많이 사용하는 방법은 g_object_get() / g_object_set() 함수를 이용하는 겁니다.
{
EdcHost *host;
gchar *address;
gint port;

g_type_init ();

host = edc_host_new ();

g_object_set (host,
"address", "192.168.0.100",
"port", 8080,
NULL);

address = edc_host_get_address (host);
g_assert_cmpstr (address, ==, "192.168.0.100");
g_free (address);

g_object_get (host,
"address", &address,
"port", &port,
NULL);

g_assert_cmpstr (address, ==, "192.168.0.100");
g_assert_cmpint (port, ==, 8080);
g_free (address);

g_object_unref (host);
}

g_object_new() 함수를 이용하여 객체를 생성할때 아예 속성을 함께 지정할 수도 있습니다.
{
EdcHost *host;
gchar *address;
gint port;

g_type_init ();

host = g_object_new (EDC_TYPE_HOST,
"address", "demo.emstone.com",
"port", 8081,
NULL);
g_object_get (host,
"address", &address,
"port", &port,
NULL);
g_assert_cmpstr (address, ==, "demo.emstone.com");
g_assert_cmpint (port, ==, 8081);
g_free (address);

g_object_unref (host);

}

눈여겨 보신 분은 아시겠지만, edc_host_new() 함수는 g_object_new (EDC_TYPE_HOST, NULL) 호출로 만들어진 객체를 돌려주는 역할만 합니다.

이렇게 대략 GObject 속성 기본 사용법을 설명한 것 같습니다. 물론 이 예제 코드에는 몇 가지 오류가 남아있는데, 이는 위에서 언급한 것처럼 객체 속성을 다루는 다른 부분을 설명하면서 보완해 나갈 예정입니다.

오늘은 여기까지입니다.

(이 글은 개인 블로그에 함께 게재되어 있습니다)

GObject 객체 지향 프로그래밍 (2)

첫번째 글이 당연한 내용을 너무 길게 설명했다는 의견이 있어서, 이번 글부터는 더 짧고 간결하게 정리해 보려고 노력하고 있습니다. 그리고, 이 글의 대상은 한 번이라도 GTK+ / GLib 라이브러리를 사용한 경험이 있는 개발자입니다. 그래서 정말로 기초적인 내용은 피하고 있습니다.

접근자 (Accessors)

소프트웨어 공학에서 모듈이나 객체 설계시 기본적으로 강조하는 정보은닉(information hiding), 캡슐화(encapsulation), 결합도(coupling) 등과 같은 개념에 의하면, C 언어처럼 구조체의 필드 변수를 외부로 직접 공개하는 건 좋지 않다고 합니다. 그리고 대부분의 경우 직접 접근 방식보다 읽고 쓰는 접근자(accessors)를 제공하는 게 여러모로 좋다고 하지요. 물론 성능 문제로 직접 접근 방식을 고려해야 하는 경우도 있지만, 지금까지 경험에 비춰보면, 병목을 일으키는 부분은 프로파일러를 돌려서 정확하게 파악한 다음에 해결하는 게 대부분 좋기 때문에 처음부터 그럴 필요는 없을 것 같습니다.

참고로 현재 개발 중인 GTK+ 3.0에서도 기존에 공개되었던 변수들을 모조리 안으로 숨기고, GTK+ 2.x 어플리케이션의 이전(migration)을 위해 GSEAL() 매크로를 2.14 버전부터 제공하고 있습니다.

아무튼 그래서, 일단 지난 글에서 예제로 사용한 호스트 객체의 필드를 숨기고 접근 API를 구현해 보았습니다. (변경되거나 수정한 부분만 보여드립니다)

edc-host.h
typedef struct _EdcHostClass EdcHostClass;
typedef struct _EdcHost      EdcHost;

struct _EdcHost
{
GObject parent;
};

struct _EdcHostClass
{
GObjectClass parent_class;
};

GType    edc_host_get_type (void) G_GNUC_CONST;
EdcHost *edc_host_new      (void);

gchar   *edc_host_get_name     (EdcHost     *host);
void     edc_host_set_name     (EdcHost     *host,
const gchar *name);
gchar   *edc_host_get_address  (EdcHost     *host);
void     edc_host_set_address  (EdcHost     *host,
const gchar *address);
gint     edc_host_get_port     (EdcHost     *host);
void     edc_host_set_port     (EdcHost     *host,
gint         port);
gchar   *edc_host_get_user     (EdcHost     *host);
void     edc_host_set_user     (EdcHost     *host,
const gchar *user);
gchar   *edc_host_get_password (EdcHost     *host);
void     edc_host_set_password (EdcHost     *host,
const gchar *password);

edc-host.c
#include "edc-host.h"

typedef struct _EdcHostPrivate EdcHostPrivate;
struct _EdcHostPrivate
{
gchar *name;
gchar *address;
gint   port;
gchar *user;
gchar *password;
};

#define EDC_HOST_GET_PRIVATE(host) \
G_TYPE_INSTANCE_GET_PRIVATE (host, EDC_TYPE_HOST, EdcHostPrivate)

G_DEFINE_TYPE (EdcHost, edc_host, G_TYPE_OBJECT);

EdcHost *
edc_host_new (void)
{
return EDC_HOST (g_object_new (EDC_TYPE_HOST, NULL));
}

/* object initializer */
static void
edc_host_init (EdcHost *host)
{
EdcHostPrivate *priv;

priv = EDC_HOST_GET_PRIVATE (host);

priv->name = g_strdup ("");
priv->address = g_strdup ("");
priv->port = 0;
priv->user = g_strdup ("");
priv->password = g_strdup ("");
}

/* object finalizer */
static void
edc_host_finalize (GObject *self)
{
EdcHost *host = EDC_HOST (self);
EdcHostPrivate *priv;

priv = EDC_HOST_GET_PRIVATE (host);

g_free (priv->name);
g_free (priv->address);
g_free (priv->user);
g_free (priv->password);

/* call our parent method (always do this!) */
G_OBJECT_CLASS (edc_host_parent_class)->finalize (self);
}

/* class initializer */
static void
edc_host_class_init (EdcHostClass *klass)
{
GObjectClass *gobj_class;

gobj_class = G_OBJECT_CLASS (klass);
gobj_class->finalize = edc_host_finalize;

g_type_class_add_private (gobj_class, sizeof (EdcHostPrivate));
}

gchar *
edc_host_get_name (EdcHost *host)
{
EdcHostPrivate *priv;

g_return_val_if_fail (EDC_IS_HOST (host), NULL);

priv = EDC_HOST_GET_PRIVATE (host);

return g_strdup (priv->name);
}

void
edc_host_set_name (EdcHost     *host,
const gchar *name)
{
EdcHostPrivate *priv;

g_return_if_fail (EDC_IS_HOST (host));
g_return_if_fail (name != NULL);

priv = EDC_HOST_GET_PRIVATE (host);

g_free (priv->name);
priv->name = g_strdup (name);
}

먼저 헤더 파일을 보면, EdcHost 구조체에서 공개되었던 객체 변수가 모두 사라지고, 대신 edc_host_{get,set}_* () 형태의 API 선언이 추가되었습니다. 소스 파일에는 새로 EdcHostPrivate 구조체를 정의하고 모든 비공개 변수를 집어 넣은 뒤, 클래스 초기화 함수[edc_host_class_init ()] 마지막 부분에서 이 크기만큼의 공간을 확보하도록 합니다.[g_type_class_add_private()] 그리고 모든 함수에서 이 구조체를 쉽게 얻어오기 위해 정의한EDC_HOST_GET_PRIVATE() 매크로를 사용해 필요한 작업을 수행합니다.

부가적으로 조금만 더 설명하면, 모든 문자열을 넘겨주는 API는 문자열을 복사해서 넘겨주어 원본 문자열을 보호합니다. 따라서 API 문서에 넘겨받은 문자열을 반드시 해제하라고 명시되어 있어야 하겠죠. 또한 지난 글에서 잠시 언급한 것처럼, 공개된 함수 진입 시점에서 인수 적합성 검사를 할때 EDC_IS_HOST() 매크로를 사용해 NULL 여부 뿐 아니라 정확하게 해당 객체인지 검사하도록 합니다.

참고로 위 예제에서 비공개(private) 객체에 접근하기 위해 사용한 방식은 오버헤드가 조금 있어서 성능이 중요한 경우 조금 다른 방식을 사용해야 합니다. 이에 대한 내용은 이전 포스트를 참고하시기 바랍니다.

이렇게 해서 기본적인 객체 속성에 대한 접근자를 구현했습니다. 물론 이게 다는 아니고, 다음에 설명할 GObject 속성(properties) 기능을 이용하면 사실 접근자를 구현할 필요도 없습니다. 하지만, GTK+와 같은 대부분의 GObject 기반 객체는 함수 API 기반의 접근자를 동시에 제공하고 있으므로 관례를 따르는 게 나쁘지는 않겠지요.

글머리에서 언급했듯이, 계속 적다 보면 내용도 길어지고 포스팅 주기도 길어질 것 같아 오늘은 일단 여기까지만 적습니다. 다음에는 본격적으로 GObject 속성(properties)을 추가할 예정인데, 설명할 게 많아서... ;)

(이 글은 개인 블로그에 함께 게재되어 있습니다)

GObject 객체 지향 프로그래밍 (1)

GTK+, Clutter 등과 같은 라이브러리는 C 언어로 구현되었지만 객체 지향 개념을 충실히 따르고 있는데, 그 중심에는 GLib 라이브러리의 GObject가 있습니다. 따라서 이러한 라이브러리를 제대로 이해하고 사용하려면 필수적으로 GObject 개념을 잘 이해하고 있어야 합니다. 그런데, 생각보다 GObject 객체는 이해하기 어렵습니다. 이해하더라도 이를 응용하려면 그만큼 시간이 또 필요합니다.

그래서 이번 글을 시작으로 GObject 라이브러리를 이용한 C 언어에서 객체 지향 프로그래밍이라는 거창한 주제를 예제 형식을 이용해 다루어 보려고 합니다. 바로 새로운 GTK+ 위젯을 구현하거나 클러터 객체를 분석하는 방식이 아니라 왜 GObject가 이런 방식으로 설계되었는지 그 철학을 따라가 보려고 합니다. 그리고, 가능한 기존 GObject 튜토리얼의 어려운 설명이 아니라 실제 사용하는 코드를 중심으로 설명할 예정이니, 그래도 무슨 말인지 모르겠거나 더 풀어서 설명을 해주는게 좋을 것 같을 경우 의견 주시기 바랍니다.

여기서 예제로 사용할 개념은 네트워크 카메라 호스트와 호스트 목록입니다. (하는 일이 이쪽 분야라서... :)

네트워크 카메라 호스트는 이름(name), 주소(address), 포트번호(port), 사용자(user), 비밀번호(password) 등과 같은 항목을 포함합니다. 필요한 함수로는 새 객체를 만들거나 해제, 그리고 각 필드값을 얻어오거나 변경하는 정도입니다. (아마도 나중에는 값이 변경되면 자동으로 호출되는 콜백 함수도 추가할 겁니다)

호스트목록은 카메라 호스트 객체 목록을 유지하면서 새 서버를 추가하거나 기존 객체를 삭제, 또는 수정하는 함수가 필요합니다. (이 역시 나중에는 목록이 변경되면 자동으로 호출되는 콜백 함수를 추가할 겁니다)

모든 코드는 GLib API를 이용하여 작성합니다.

객체 (Objects) + 참조 카운터 (Reference Counter)


소프트웨어 공학자들이 객체라고 부르기 전부터 C 언어에는 구조체(struct)가 있었습니다. GObject 시스템 역시 기본 바탕은 구조체입니다. 그러면 GObject 프로그래밍을 하기 전에, 일반 C 언어 구조체를 이용해 네트워크 카메라 호스트를 정의하면, 다음과 같은 코드가 나오지 않을까요?
typedef struct _EdcHost EdcHost;
struct _EdcHost
{
gchar *name;
gchar *address;
gint  port;
gchar *user;
gchar *password;
};

만일 상속이나 함수 오버로딩(overloading)을 전혀 사용하지 않는다면, 굳이 새로운 함수를 추가할 필요를 못 느끼는 분들이 많을 겁니다. 왜냐하면, 직접 구조체 크기만큼 메모리를 할당한 뒤 해제하고, 직접 모든 필드를 접근하면 되니까요. 하지만, 할당하고 해제하는 코드가 여러 곳에 분산되어 있다면 디버깅도 힘들고 유지 보수도 힘드니까 최소한 객체를 생성하고 해제하는 함수 만이라도 만들어 봅시다.
EdcHost *
edc_host_new (void)
{
EdcHost *host;

host = g_new0 (EdcHost, 1);

return host;
}

void
edc_host_destroy (EdcHost *host)
{
g_return_if_fail (host != NULL);

g_free (host->name);
g_free (host->address);
g_free (host->user);
g_free (host->password);
g_free (host);
}

간단한 코드라서 설명할 필요는 없을 것 같습니다. 참고로 g_free() 함수는 인수가 NULL일 경우 무시하므로 NULL 검사 코드는 필요없습니다.

그런데, 이 객체는 단순히 목록 관리 뿐 아니라 여러 다른 모듈에서도 사용할 예정입니다. 여기서 갑자기, 모든 모듈이 하나의 객체를 공유하고 싶은 욕망이 꿈틀대기 시작합니다. 모듈 간에 객체를 전달할때 복사할 필요도 없고, 모듈 별로 객체를 따로 만들어 정보를 보관하는 것보다 메모리를 절약할 수 있으며, 필드 하나가 변경되었을 경우 그 정보를 모든 관련 객체에 반영할 수고도 덜 수 있기 때문입니다. 그렇다고 무턱대고 모든 모듈에서 객체 주소(pointer)만 참조하게 하면 객체를 어느 시점에 할당하고 해제해야 하는지 매우 까다로워집니다. 특히 동적으로 임시 객체를 생성해 다른 모듈에게 넘겨주는 경우라면, 객체를 어느 시점에서 해제해야 하는지도 실수하기 딱 좋습니다. 더 나아가 멀티 쓰레드 환경까지 고려한다면, 단순히 포인터만 가리키는 방식은, 아마추어나 사용하는 옛날 UML 클래스 빌더가 자동으로 생성해주는 코드만으로는, 힘들 수 밖에 없습니다.

이런 경우 자주 사용하는 방식이 참조 카운터(reference counter) 기법입니다. 짧게 설명하자면, 모든 모듈에서 몇 가지 원칙만 지키면 됩니다. 첫번째 원칙은, 객체(메모리)를 할당한 모듈에서 반드시 해제하기입니다. 두번째는, 모듈 관점에서 내가 필요한 시점부터 객체의 참조 카운터를 증가하고, 더이상 사용하지 않으면 객체의 참조 카운터를 감소합니다. 새로 생성된 객체는 항상 참조 카운터 1을 가지고 있기 때문에, 참조 카운터가 감소되어 0이 되면 객체는 자동으로 해제됩니다. 참고로, 참조 카운터 기법은 멀티미디어 프레임, 네트워크 패킷 등과 같은 버퍼 관리에도 널리 사용하는 것은 물론, 오브젝티브-C 언어(Objective-C)의 NSObject 객체가 기본적으로 제공하는 기능이기도 합니다.

자 이제, 호스트 객체를 참조 카운터 기법을 적용해 수정해 보면 다음과 같습니다.

edc-host.h
#ifndef __EDC_HOST_H__
#define __EDC_HOST_H__

#include <glib.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef struct _EdcHost EdcHost;
struct _EdcHost
{
gchar *name;
gchar *address;
gint port;
gchar *user;
gchar *password;

gint ref_count;
};

EdcHost *edc_host_new (void);
EdcHost *edc_host_ref (EdcHost *host);
void edc_host_unref (EdcHost *host);

#ifdef __cplusplus
}
#endif

#endif /* __EDC_HOST_H__ */

edc-host.c
#include "edc-host.h"

EdcHost *
edc_host_new (void)
{
EdcHost *host;

host = g_new0 (EdcHost, 1);
if (!host)
return NULL;

host->ref_count = 1;

return host;
}

static void
edc_host_destroy (EdcHost *host)
{
g_return_if_fail (host != NULL);

g_free (host->name);
g_free (host->address);
g_free (host->user);
g_free (host->password);
g_free (host);
}

EdcHost *
edc_host_ref (EdcHost *host)
{
g_return_val_if_fail (host != NULL);

g_atomic_int_inc (&host->ref_count);

return host;
}

void
edc_host_unref (EdcHost *host)
{
g_return_if_fail (host != NULL);

if (g_atomic_int_dec_and_test (&host->ref_count))
edc_host_destroy (host);
}

제일 먼저 설명할 부분은 역시 g_atomic_int_inc() / g_atomic_int_dec_and_test() 함수입니다. 멀티 쓰레드에서 안전하게 카운터 변수를 증가하고 감소할 수 있게 도와주는 GLib API입니다. 이를 이용해 위에서 설명한 참조 카운터 개념을 구현하고 있습니다. 공개했던 edc_host_destroy() 함수는 모듈 내부에서만 접근할 수 있도록 static 키워드를 붙였습니다. 또한 C++ 소스에서 포함(include)할때 문제를 일으키지 않도록 헤더파일에 'extern "c" {}' 키워드도 추가했습니다.

그런데 참조 카운터가 필요한 객체마다 이렇게 구현하면 비슷한 작업을 하는 코드가 중복될 수 밖에 없습니다. 이를 일반적인 API로 분리해 다시 구현하면 재활용이 가능할테니, 다음과 같이 수정해 보겠습니다.

edc-object.h
#ifndef __EDC_OBJECT_H__
#define __EDC_OBJECT_H__

#include <glib.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef struct _EdcObject EdcObject;
struct _EdcObject
{
gint ref_count;
GDestroyNotify finalize;
};

static inline gpointer
edc_object_alloc (GDestroyNotify finalize,
gint obj_size)
{
EdcObject *obj;

obj = g_malloc (obj_size);
if (!obj)
return NULL;

obj->ref_count = 1;
obj->finalize = finalize;

return obj;
}

static inline gpointer
edc_object_ref (gpointer obj)
{
EdcObject *object = obj;

if (object)
g_atomic_int_inc (&object->ref_count);

return object;
}

static inline void
edc_object_unref (gpointer obj)
{
EdcObject *object = obj;

if (!obj)
return;

 if (g_atomic_int_dec_and_test (&object->ref_count))
{
if (object->finalize)
object->finalize (object);
g_free (object);
}
}

#ifdef __cplusplus
}
#endif

#endif /* __EDC_OBJECT_H__ */

edc-host.h
#ifndef __EDC_HOST_H__
#define __EDC_HOST_H__

#include "edc-object.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct _EdcHost EdcHost;
struct _EdcHost
{
EdcObject parent;

gchar *name;
gchar *address;
gint port;
gchar *user;
gchar *password;
};

EdcHost *edc_host_new (void);

#ifdef __cplusplus
}
#endif

#endif /* __EDC_HOST_H__ */

edc-host.c
#include "edc-host.h"

static void
edc_host_finalize (gpointer obj)
{
EdcHost *host = obj;

g_free (host->name);
g_free (host->address);
g_free (host->user);
g_free (host->password);
}

EdcHost *
edc_host_new (void)
{
EdcHost *host;

host = edc_object_alloc (edc_host_finalize,
sizeof (EdcHost));
if (!host)
return NULL;

host->name = NULL;
host->address = NULL;
host->user = NULL;
host->password = NULL;

return host;
}

객체 지향 상속(또는 파생 객체)을 C 언어로 구현하는 가장 쉬운 방법은 위 코드에서 보는 것처럼 부모(또는 원본 객체)를 구조체 맨 앞에 두는 겁니다. 그러면 부모와 자식 API 모두 사용할 수 있게 되죠. 위 코드의 경우 개념상으로 보면 EdcObject 객체를 상속 받아 EdcHost 객체를 구현한 셈이 되죠. 따라서 다음과 같이 사용할 수 있습니다.
void
func_a (EdcHost *host)
{
edc_object_ref (host);
// do some stuff for long time...
edc_object_unref (host);
}

{
EdcHost *host;

host = edc_host_new ();
...
func_a (host);
...
edc_object_unref (host); /* destroy */
}

참고로 C 언어에서 `void *' 형은 어떤 포인터와도 양방향 대입(assignment)을 할 수 있으므로 컴파일 경고를 피하기 위해 불필요한 형변환을 할 필요가 없습니다. (gpointer / GDestroyNotify API도 설명도 확인해 보시기 바랍니다)

이제 지금까지 구현한 부분을 GObject 객체 기반으로 옮겨 봅니다. 자세히 보시면, 지금까지 프로그래밍한 내용과 거의 비슷한 점을 알아챌 수 있을 겁니다.

edc-host.h
#ifndef __EDC_HOST_H__
#define __EDC_HOST_H__

#include <glib-object.h>

G_BEGIN_DECLS

#define EDC_TYPE_HOST \
(edc_host_get_type ())
#define EDC_HOST(obj) \
(G_TYPE_CHECK_INSTANCE_CAST ((obj), EDC_TYPE_HOST, EdcHost))
#define EDC_HOST_CLASS(obj) \
(G_TYPE_CHECK_CLASS_CAST ((obj), EDC_TYPE_HOST, EdcHostClass))
#define EDC_IS_HOST(obj) \
(G_TYPE_CHECK_INSTANCE_TYPE ((obj), EDC_TYPE_HOST))
#define EDC_IS_HOST_CLASS(obj) \
(G_TYPE_CHECK_CLASS_TYPE ((obj), EDC_TYPE_HOST))
#define EDC_GET_HOST_CLASS(obj) \
(G_TYPE_INSTANCE_GET_CLASS ((obj), EDC_TYPE_HOST, EdcHostClass))

typedef struct _EdcHostClass EdcHostClass;
typedef struct _EdcHost      EdcHost;

struct _EdcHost
{
GObject parent;

gchar *name;
gchar *address;
gint   port;
gchar *user;
gchar *password;
};

struct _EdcHostClass
{
GObjectClass parent_class;
};

GType    edc_host_get_type (void) G_GNUC_CONST;
EdcHost *edc_host_new      (void);

G_END_DECLS

#endif /* __EDC_HOST_H__ */

edc-host.c
#include "edc-host.h"

G_DEFINE_TYPE (EdcHost, edc_host, G_TYPE_OBJECT);

EdcHost *
edc_host_new (void)
{
return EDC_HOST (g_object_new (EDC_TYPE_HOST, NULL));
}

/* object initializer */
static void
edc_host_init (EdcHost *host)
{
host->name = NULL;
host->address = NULL;
host->port = 0;
host->user = NULL;
host->password = NULL;
}

/* object finalizer */
static void
edc_host_finalize (GObject *self)
{
EdcHost *host = EDC_HOST (self);

g_free (host->name);
g_free (host->address);
g_free (host->user);
g_free (host->password);

/* call our parent method (always do this!) */
G_OBJECT_CLASS (edc_host_parent_class)->finalize (self);
}

/* class initializer */
static void
edc_host_class_init (EdcHostClass *klass)
{
GObjectClass *gobject_class;

gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = edc_host_finalize;
}

갑자기 코드량이 증가했다고 놀랄 필요는 없습니다. 뭐든지 다 그렇지만, 알고 보면 별 거 아닙니다.

먼저 헤더 파일을 설명하면,  GObject 객체를 사용하기 위해 glib-object.h 파일을 포함했습니다. 이는 EdcHost 객체가 GObject 객체만 사용하기 때문에, 더 정확히는 GObject의 파생 객체(derived objects), 다른 말로는 GObject 객체만 상속(inheritance)하기 때문에 그렇습니다. 만일 다른 객체에서 파생한다면 그 객체를 정의하는 헤더 파일을 포함해야 합니다. 'extern "c" {}' 키워드는 GLib의 G_BEGIN_DECLS / G_END_DECLS API로 대체했습니다.

EdcHost 인스턴스와 EdcHostClass 클래스를 정의하고 있는데, 당연히 클래스는 전역으로 하나만 존재하고 그냥 객체는 인스턴스(instance) 역할을 합니다. 또한 여기서는 인스턴스 객체의 모든 필드가 공개되어 있지만, 물론 외부에 공개하지 않는(private) 필드를 정의할 수도 있습니다. (이는 다른 글에서 따로 설명하겠습니다)

복잡해 보이는 몇몇 매크로는 자주 사용하는 긴 API를 간편화한 것입니다. 런타임 중에 인스턴스가 유효하고 EdcHost 객체로 형변환까지 해주거나[EDC_HOST(obj)], 인스턴스가 EdcHost 객체인지 확인하거나[EDC_IS_HOST(obj)], 인스턴스의 클래스 객체를 얻어오거나[EDC_GET_HOST_CLASS(obj)] 하는 등 일종의 RTTI 관련 매크로입니다. 아마 제일 많이 사용하는 매크로는 `EDC_HOST(obj)'일 겁니다.

소스를 살펴 보면, 제일 먼저 나오는게 `G_DEFINE_TYPE(TN, t_n, T_P)' 입니다. 여담이지만, 이 매크로가 추가되기 전에 작성한 GObject 기반 코드는 귀찮은 작업을 많이 해야 했는데, 이 매크로가 자동으로 해주는 기능이 많아서 불필요하게 중복되는 코드가 많이 줄어들었습니다. 그래서 GTK+ 소스 코드 중에도 가끔 그렇게 작성한 코드도 있고, GObject 관련 초기 문서를 보면 이 매크로를 사용하지 않고 구현되어 있는 경우도 있습니다.

이 매크로가 하는 일은 다음과 같습니다. 지정한 `t_n` 이름으로 시작하는 클래스 초기화 함수[*_class_init()] / 인스턴스 초기화 함수[*_init()] 모두 구현되어 있다고 가정하고 `*_get_type()' 함수를 자동으로 삽입해 줍니다. 더불어 부모 클래스 객체를 가리키는 `*_parent_class' 전역 변수도 만들어 줍니다. 따라서 프로그래머는 최소한 함수 두 개만 구현해 주면 되는 셈입니다. [edc_host_init() / edc_host_class_init()]

하지만 위 예제에서는 클래스 초기화 함수에서 인스턴스 객체가 해제될때 호출되는 finalize 함수를 교체하고 있습니다. 이를 통해 객체가 해제될때 사용하던 리소스를 해제해 줍니다. 그리고, 반드시 상위 클래스의 finalize 함수를 호출해 주어야 정상적으로 부모 객체의 해제 함수가 차례대로 호출될 수 있습니다.

자 이제 GObject의 핵심 기능 중 하나인 객체 참조 카운터(object reference counter) 기능을 쉽게 이용할 수 있습니다. 이렇게 작성한 객체는 g_object_ref() / g_object_unref() 함수 등을 이용해 참조 카운터를 제어할 수 있습니다. GObject 소스 코드를 확인해 보시면 알겠지만, 실제 객체 참조 카운터 기능은 거의 비슷하게 구현되어 있습니다. 더 많은 경우의 수를 고려하고 더 많은 기능을 제공하다보니 코드가 더 복잡한 것 뿐입니다.

더 중요한 점은 모든 GObject 기반 객체, 예를 들어 GTK+ 위젯이나 클러터 객체 모두 GObject 기반이기 때문에 객체간 연결(부모-자식, 컨테이너-아이템 등)시 객체에 대한 포인터를 유지하면서 동시에 참조 카운터를 유지하여 메모리를 관리한다는 점입니다. 이 부분에 대한 더 자세한 설명은 GTK+ 메모리 관리 글에서 확인하시기 바랍니다.

오늘은 일단 여기까지만... ;)

(이 글은 개인 블로그에 함께 게재되어 있습니다)

GLib 쓰레드 프로그래밍

소프트웨어를 개발하면서 멀티 쓰레드 방식을 사용하는 경우는 많습니다. 하지만 그만큼 복잡도가 증가해서 세심하게 고려하여 설계하지 않으면 디버깅 재앙을 얻는 경우가 많습니다. 이 글은 '멀티쓰레드 프로그래밍 규칙'에서 이어지는 내용입니다. GTK+ 쓰레드 관련 잡설은 이미 언급한 적이 있으니까, 오늘은 별도의 쓰레드로 동작하는 간단한 예제 모듈을 만들면서 몇가지 유용한 GLib 쓰레드 API를 설명하겠습니다.

리소스 (Resources)

한 개 이상의 쓰레드가 동작하는 방식의 소프트웨어를 설계할 경우 가장 염두에 두어야 하는 점은 자원(resources)입니다. 자원, 즉 리소스는 쉽게 말해 소프트웨어가 사용하는 데이터를 의미합니다. 전역 변수, 디스크 파일, 네트웍 소켓, 외부 장치 심지어 비디오 카드 같은 그래픽 장치 등이 모두 리소스입니다. 물론 넓은 의미에서 보면 리소스는 하나의 기능이나 세부 작업을 나타낼 수도 있습니다.

멀티쓰레드 프로그래밍에서 가능한 지켜야 하는 가장 중요한 원칙은 '하나의 쓰레드만 하나의 리소스에 접근할 수 있어야 한다'입니다. 아무 생각없이 하나의 리소스에 여러 쓰레드가 동시에 접근하도록 설계할 경우, 어쩔 수 없이 뮤텍스(mutex) 계열 API를 이용해 접근할 때마다 임계 구역을 보호해야 합니다. 그리고 이러한 기법은 소스 코드가 복잡해지고 커질수록 버그가 많아지고, 디버깅도 점점 어려워집니다. 물론, 쓰레드-풀(thread-pool) 기법처럼 성능 최적화나 확장성을 위해 멀티쓰레드를 사용하는 경우처럼 예외도 사실 많지만, 일단 이 글에서는 무시합니다.

앞서 예를 들었던 GTK+ 쓰레드 프로그래밍도 리소스 관점에서 보면, 무조건 모든 쓰레드에서 GTK+ / GDK API 호출 전후에 gdk_threads_*() 계열 API를 남용해서 지독한 데드락과 이중락에 고생하던가, 아니면 GTK+ / GDK API 호출을 메인 쓰레드에서만 호출하도록 g_idle_add() / g_timeout_add() API만 이용하는 방법이 있습니다. 두번째 방법을 모델-뷰(Model-View) 개념으로 생각하면 마지막 GTK+ / GDK API 호출을 뷰(view) 갱신으로 볼 수 있고, g_idle_add() 계열 API는 일종의 메시지 전달로 생각할 수도 있습니다. (참고로 Sentry24DVR 2.x 버전은 첫번째 방식을, Sentry24CMS 2.x 버전은 두번째 방식을 사용합니다)

쓰레드 시작 / 정지 / 실행

가장 먼저 쓰레드를 만들고 종료하는 루틴을 만들어 봅시다. 편의상 모듈 이름은 'drink'라고 합니다.
#include <glib.h>

typedef struct _Drink Drink;
struct _Drink
{
GThread *thread;
gint running;
GAsyncQueue *queue;
gchar *host;
gint port;
};

static gpointer
drink_process (gpointer data)
{
Drink *drink = data;

while (g_atomic_int_get (&drink->running))
{
// do something...
}

return NULL;
}

Drink *
drink_new (const gchar *host, gint port)
{
Drink *drink;

g_return_val_if_fail (host != NULL, NULL);
g_return_val_if_fail (port > 0, NULL);

drink = g_new (Drink, 1);
drink->host = g_strdup (host);
drink->port = port;
drink->queue = g_async_queue_new ();

g_atomic_int_set (&drink->running, 1);
drink->thread = g_thread_new (drink_process, drink, TRUE, NULL);

return drink;
}

void
drink_destroy (Drink *drink)
{
g_return_if_fail (drink != NULL);

g_atomic_int_set (&drink->running, 0);
g_thread_join (drink->thread);

g_async_queue_unref (disk->queue);
g_free (drink->host);
g_free (drink);
}

drink_new() 함수는 지정한 호스트 / 포트 번호를 이용하여 새로운 Drink 객체를 만듭니다. 그리고 앞으로 나올 모든 데이터는 각각 자신이 속한 Drink 객체만 접근합니다. 즉, Drink 객체를 하나의 리소스로 여기면 됩니다. drink_destroy() 함수는 쓰레드가 종료할때까지 기다렸다가 Drink 객체를 해제하고 마무리합니다.

쓰레드 함수 무한 루프는 간단하게 정수형 변수를 플래그처럼 사용합니다. 제대로 하려면 플래그 변수 역시 뮤텍스 API로 보호해주어야 하지만 대부분의 경우 간단한 원자연산자(atomic operator)로 처리가 가능합니다. 일단 이렇게 만들어 둡시다.

마지막으로 설명할 API가 GAsyncQueue 객체인데, 가장 중요한 역할을 담당하는 물건입니다. 설명 그대로 이 API는 쓰레드간 비동기 통신(asynchronous communication between threads)을 하는데 사용합니다. 이 객체를 생성하는데는 g_async_queue_new(), 없애기 위해서는 g_async_queue_unref() 함수를 이용하는데, 일단 지금은 만들어만 놓습니다.

API 추가 + 메시지 전달

제일 먼저 하고 싶은 일은 미리 지정한 서버에 TCP 연결을 하거나, 끊고 싶습니다. 이를 비동기큐를 이용해서 간단하게 구현해 봅시다.
enum
{
DRINK_MSG_CONNECT = 1,
DRINK_MSG_SHUTDOWN = 2,
};

void
drink_connect (Drink *drink)
{
g_return_if_fail (drink != NULL);

g_async_queue_push (drink->queue,
GINT_TO_POINTER (DRINK_MSG_CONNECT));
}

void
drink_shutdown (Drink *drink)
{
g_return_if_fail (drink != NULL);

g_async_queue_push (drink->queue,
GINT_TO_POINTER (DRINK_MSG_SHUTDOWN));
}

이렇게 하면 drink_connect() / drink_shutdown() 함수를 호출하면 g_async_queue_push() 함수를 이용해 메시지를 큐에 넣기만 하고 아무 일도 안합니다. (참고 : 모듈 외부에서 볼때는 내부 구현에 쓰레드를 사용하는지, 메시지 큐를 이용하는지 등은 공개되지도 않고, 공개할 필요도 없습니다) 이제 drink_process() 함수를 다음과 같이 수정합니다.
static gpointer
drink_process (gpointer data)
{
Drink *drink = data;

while (g_atomic_int_get (&drink->running))
{
do {
GTimeVal tval;
gpointer msg;

/* wait for messages */
g_get_current_time (&tval);
g_timeval_add (&tval, 10000); /* 10msec */
msg = g_async_queue_timed_pop (drink->queue, &tval);
if (!msg)
break;

switch (GPOINTER_TO_INT (msg))
{
case DRINK_MSG_CONNECT:
// do connect work...
break;
case DRINK_MSG_SHUTDOWN:
// do shutdown work...
break;
default:
g_warning ("unknown drink msg");
break;
}
} while (1);

// do something else ...
}

return NULL;
}

보는 바와 같이 메시지 큐에서 메시지를 꺼내어 메시지에 해당하는 작업을 처리합니다. 만일 메시지 큐를 사용하지 않고 drink_connect() 함수에서 직접 연결 작업을 수행하면 쓰레드 부분과 공유하는 부분을 모두 뮤텍스로 보호해야 하지만 이처럼 모든 작업을 담당 쓰레드가 처리하도록 메시지만 전송하면 실행 순서도 맞고 쓰레드가 자료 공유를 걱정할 필요도 없게 됩니다.

여기서 사용한 g_async_queue_timed_pop() 함수는 지정한 시간 동안 아무 메시지도 없으면 NULL을 돌려줍니다. 비슷한 함수로 g_async_queue_pop() 함수는 메시지가 올때까지 무한정 기다랍니다. g_async_queue_try_pop() 함수는 메시지가 없을 경우 바로 NULL을 돌려줍니다. 만일 쓰레드 함수 자체적인 작업은 없고 100% 외부에서 메시지가 올때만 작업이 수행된다면 g_async_queue_pop() 함수를 사용하는 것이 더 좋습니다. 프로세스 동기화나 수면 상태(sleep) 등을 다른 작업을 하면서 자체적으로 하는 경우라면 g_async_queue_try_pop() 함수가 유용합니다.

이 예제에서는 단순하게 10 밀리초 여유를 두고 메시지를 확인하고, 그외 다른 작업을 처리하도록 했습니다.

쓰레드 종료 다듬기

예제 처음에 있던 쓰레드 종료 코드가 너무 단순해서 조금 불안할 지도 모르겠네요. 메시지 큐에 데이터가 있을때 종료되면 메모리 누수도 있을 것 같고... 그래서 쓰레드 종료도 하나의 메시지로 처리하도록 하려고 합니다. 수정하는 부분은 다음과 같습니다.
enum
{
DRINK_MSG_STOP_THREAD = -1,
DRINK_MSG_CONNECT = 1,
DRINK_MSG_SHUTDOWN = 2,
};

static gpointer
drink_process (gpointer data)
{
Drink *drink = data;

while (TRUE)
{
GTimeVal tval;
gpointer msg;

/* wait for messages */
g_get_current_time (&tval);
g_timeval_add (&tval, 10000); /* 10msec */
msg = g_async_queue_timed_pop (drink->queue, &tval);
if (msg)
{
if (msg == GINT_TO_POINTER (DRINK_MSG_STOP_THREAD)) break;

switch (GPOINTER_TO_INT (msg))
{
case DRINK_MSG_CONNECT:
// do connect work...
break;
case DRINK_MSG_SHUTDOWN:
// do shutdown work...
break;
default:
g_warning ("unknown drink msg");
break;
}
}

// do something else ...
}

void
drink_destroy (Drink *drink)
{
g_return_if_fail (drink != NULL);

g_async_queue_push (drink->queue, GINT_TO_POINTER (DRINK_MSG_STOP_THREAD));
g_thread_join (drink->thread);

g_async_queue_unref (disk->queue);
g_free (drink->host);
g_free (drink);
}

쓰레드 함수 무한루프 조건문이 조금 변경되었을 뿐 기본적인 원리는 동일합니다.

마지막, 조금 더 개선...

이놈의 메시지 방식을 사용하면 대부분 프로그래머는 쉽게 switch() 문의 유혹을 떨쳐버리지 못합니다. 근데, 만일 당신이 매우 성능 좋은 메시징 서비스를 만들고 있다면 이런 방식의 코드는 유지보수도 힘들고 성능도 나쁠 수 있습니다. 메시지-함수 테이블을 유지해도 되고, 여러가지 방법이 있겠지만 여기서는 약간 가독성(readability)과 유지보수에 중점을 둔 방식을 설명하려 합니다.
typedef struct _DrinkMsg DrinkMsg;
struct _DrinkMsg
{
void (*func) (Drink *drink, gpointer data1, gpointer data2);
gpointer data1;
gpointer data2;
};

static gpointer
drink_process (gpointer data)
{
...
DrinkMsg *msg;

msg = g_async_queue_try_pop (drink->queue);
if (msg)
{
if (msg == GINT_TO_POINTER (-1))
break;
msg->func (drink, msg->data1, msg->data2);
g_slice_free1 (msg);
}
...
}

void
drink_destroy (Drink *drink)
{
...
g_async_queue_push (drink->queue, GINT_TO_POINTER (-1));
g_thread_join (drink->thread);
...
}

static void
drink_connect_real (Drink *drink, gpointer data1, gpointer data2)
{
gchar *host = data1;
gint port = GPOINTER_TO_INT (data2);

// do connect work...

g_free (host);
}

void
drink_connect (Drink *drink, const gchar *host, gint port)
{
DrinkMsg *msg;

msg = g_slice_new (DrinkMsg);
msg->func = drink_connect_real;
msg->data1 = g_strdup (host);
msg->data2 = GINT_TO_POINRTER (port);
g_async_queue_push (drink->queue, msg);
}

뭐... 더 이상의 설명은 피곤해서...

궁금한 API는 직접 매뉴얼을 한 번 뒤져보시길... :)

(이 글은 개인 블로그에 함께 게재되어 있습니다)

GLib 메인루프 이용하기

GLib API를 이용한 멀티쓰레드 프로그래밍에서 비동기 메시지 큐를 이용하는 방법은 지난 포스트에서 설명한 적이 있는데, 이번에는 애플 GCD의 libdispatch와 비교되는 GLib의 메인루프를 이용하는 방법을 정리해 보았습니다. 이 방법은 어떤 관점에서 보면 더 쉽고, 이미 많은 기능이 기본적으로 지원되기 때문에 몇몇 경우를 제외하면 더 좋은 방법입니다. 다만 API 사용법을 이해하기가 처음에 조금 까다롭다는 점이 걸림돌입니다.

일반적으로 GLib / GTK 어플리케이션은 메인 쓰레드에서 실행되는 메인 이벤트 루프 기반에서 동작합니다. 키보드 / 마우스 이벤트 처리, 화면 표시, 사용자가 등록한 Idle / Timeout 함수 처리 등이 모두 이 메인 이벤트 루프에서 처리됩니다. 그런데 이 메인 이벤트 루프라는 건 마냥 개념적인게 아니라, 실제로 GMainLoop 객체를 기반으로 동작합니다. 그런데 g_main_loop_*() 계열 함수를 살펴보면 몇 개 안됩니다. 루프 객체를 생성하고, 참조하고, 해제하고, 돌리고[g_main_loop_run()], 종료하고[g_main_loop_quit()], 돌아가는 중인지 확인하기 등의 함수만 있습니다. 아, 하나 더 있군요. 객체를 생성할때 전달하는 GMainContext 객체를 얻어오는 함수[g_main_loop_get_context()]가 있군요.

모든 GMainLoop는 하나의 GMainContext와 함께 사용됩니다. GMainContext 객체는 실행할 소스[GSource] 목록을 관리합니다. 소스는 파일, 파이프, 소켓 등의 디스크립터를 기반으로 한 이벤트 소스일 수도 있고, Idle / Timeout 등과 같은 시간 소스일 수도 있습니다. 컨텍스트는 실행 소스 각각을 검사해서 원하는 이벤트가 발생했는지, 아니면 실행할 시간이 되었는지를 판단해 등록한 콜백함수를 호출합니다. 참고로, 메인 쓰레드에서 동작하기 위한 컨텍스트[g_main_context_default()]는 기본적으로 제공합니다. 이 기본 컨텍스트는 gtk_main() 함수가 사용하는 것은 물론, g_idle_add(), g_timeout_add() 등과 같은 함수도 이 기본 컨텍스트를 사용합니다.

아무튼 조금 더 구체적이고 자세한 내용은 공식 문서를 참고하시고, 이제 이를 이용한 멀티쓰레드 프로그래밍을 해보겠습니다. 말이 길었으니 코드를 먼저 보여드리겠습니다.
#include <glib.h>

static GThread *my_thread;
static GMainLoop *my_loop;

static void
add_idle_to_my_thread (GSourceFunc    func,
gpointer       data)
{
GSource *src;

src = g_idle_source_new ();
g_source_set_callback (src, func, data, NULL);
g_source_attach (src,
g_main_loop_get_context (my_loop));
g_source_unref (src);
}

static void
add_timeout_to_my_thread (guint          interval,
GSourceFunc    func,
gpointer       data)
{
GSource *src;

src = g_timeout_source_new (interval);
g_source_set_callback (src, func, data, NULL);
g_source_attach (src,
g_main_loop_get_context (my_loop));
g_source_unref (src);
}

static gpointer
loop_func (gpointer data)
{
GMainLoop *loop = data;

g_main_loop_run (loop);

return NULL;
}

static void
start_my_thread (void)
{
GMainContext *context;

context = g_main_context_new ();
my_loop = g_main_loop_new (context, FALSE);
g_main_context_unref (context);

my_thread = g_thread_create (loop_func, my_loop, TRUE, NULL);
}

static void
stop_my_thread (void)
{
g_main_loop_quit (my_loop);
g_thread_join (my_thread);
g_main_loop_unref (my_loop);
}

함수 먼저 설명하면, start_my_thread() 함수는 쓰레드를 시작하고, stop_my_thread() 함수는 쓰레드를 중지합니다. add_idle_to_my_thread() 함수는 바로 실행되는 Idle 콜백 함수를 추가하고, add_timeout_to_my_thread() 함수는 주기적으로 실행되는 Timeout 콜백 함수를 추가합니다. 마지막 두 함수의 인수는 g_idle_add(), g_timeout_add() 함수와 각각 동일합니다. 따라서, 콜백 함수가 TRUE를 리턴하면 자동으로 반복해서 계속 실행되고, FALSE를 리턴하면 한번만 실행되고 종료합니다.

위 코드의 핵심은 GMainContext 객체를 만들고 이를 기반으로 GMainLoop 객체를 만든 뒤 별도 쓰레드에서 실행하도록 하는 부분입니다. 그리고, 필요한 모든 작업은 Idle / Timeout 소스 객체를 만들어 컨텍스트에 추가(attach)해서 동작하도록 하는 겁니다. 참고로, 관련 API는 모두 쓰레드에 안전합니다.

물론 위 함수를 조금 더 확장하면 콜백함수가 종료될때 자동으로 호출되는 notify 함수도 등록할 수 있고, 우선순위도 조절할 수 있습니다. 또한 여러 쓰레드를 종류별로 만들어 필요한 쓰레드에게 해당 작업만 전달해도 됩니다. 하지만 그 정도는 응용하는데 별로 어려움이 없을 거라 생각하고 한가지만 더 설명하겠습니다.

예를 들어 네트워크 소켓(socket)을 하나 만들고 이 소켓에 읽을 데이터가 도착했을 경우에만 호출되는 함수를 등록하고 싶은 경우, 다음과 같은 코드를 사용하면 됩니다.
static gboolean
socket_read (GIOChannel *source,
GIOCondition condition,
gpointer data)
{
/* Use g_io_channel_read_chars() to read data... */

return TRUE;
}

static void
add_socket_to_my_thread (gint sock_fd)
{
GIOChannel *channel;
GSource *src;

channel = g_io_channel_unix_new (sock_fd);
src = g_io_create_watch (channel, G_IO_IN);
g_source_set_callback (src,
(GSourceFunc) read_socket,
NULL,
NULL);
g_source_attach (src,
g_main_loop_get_context (my_loop));
g_source_unref (src);
}

자세한 내용은 위 코드와 비슷하지만 기본 메인 이벤트 루프에서 동작하도록 하는 g_io_add_watch() API 설명 부분을 참고하시기 바랍니다. 어쨌든, 기본적으로 GMainContext 객체는 유닉스 시스템의 폴링(polling) 메카니즘을 사용하기 때문에 이론적으로는 거의 모든 파일 디스크립터를 사용할 수 있습니다. 물론 비슷한 방식으로 윈도우 운영체제에서 이벤트 핸들이나 소켓 핸들도 사용할 수도 있습니다.

글머리에서 적은 것처럼 비동기 메시지 큐를 이용하는 방식보다 아주 약간의 오버헤드는 있겠지만, 훨씬 더 많은 기능을 제공하는 것 같지 않나요?

(이 글은 개인 블로그에 함께 게재되어 있습니다)

GLib 테스트 프레임워크 사용하기

GLib 라이브러리 2.16 버전부터 지원하는 테스트 프레임워크는 C 언어용 유닛테스트 도구입니다. 물론 많은 유닛 테스트 도구가 이미 존재하지만, GLib 라이브러리 기반 C 언어 프로그램이라면 굳이 다른 라이브러리를 사용하는 것보다는 이미 지원하는 훌륭한 도구를 사용하는게 더 좋겠지요. 참고로, GTK+, Clutter 등 같은 프로젝트도 이미 이 기능을 이용해 테스트 코드를 작성하고 있으므로 알아두면 도움이 됩니다. 모든게 그렇지만, 알고나면 별게 아니므로 기본 개념과 API 사용법만 충실히 이해하면 됩니다.

기본 개념 및 사용법


유닛테스트 개념은 스몰토크, 자바, C++처럼 언어적으로 객체지향 개념을 지원하는 언어에서 시작했기 때문에 C 언어에 그대로 적용하기에는 조금 까다로운 점이 많습니다. 그래서 GLib 테스트 프레임워크는 유닛테스트에서 기본 개념과 테스트 실행 방식만 빌려옵니다. 우선 알아야하는 기본 개념은 다음과 같습니다.

  • 테스트 케이스 (Test Case) : 가장 기본이 되는 하나의 테스트 단위입니다. GLib에서는 하나의 테스트 함수(function)가 이 역할을 합니다.

  • 픽스쳐 (Fixture) : 고정 설치된 물건이라는 뜻처럼, 테스트 케이스 실행 전후에 항상 실행하는 함수를 의미합니다. 실제로는, 테스트 함수를 실행하기 위해 필요한 환경을 미리 구축하거나(setup) 실행 후 리소스를 정리하는(teardown) 함수, 그리고 이와 함께 사용되는 사용자 데이터(data)로 구성됩니다. 참고로, GLib에서는 각 테스트간 의존성을 피하기 위해 모든 테스트 케이스를 실행할때마다 매번 픽스쳐를 새로 구성하는 방식(fresh fixture)을 사용합니다.

  • 테스트 슈트 (Test Suite) : 여러 테스트 케이스를 묶은 그룹입니다. 트리 구조처럼 테스트 슈트 여러개를 묶어 더 큰 테스트 슈트를 구성할 수도 있습니다. GLib에서는 테스트 경로(path)라는 개념으로 사용합니다.


개념은 조금 복잡한 것 같지만, 복잡하고 다양한 테스트 케이스를 그룹화하면 나중에 테스트 슈트별로 테스트를 진행할 수도 있는 등 많은 장점이 있습니다. 그리고 GLib이 제공하는 커맨드라인 도구를 이용하면 테스트 결과를 XML로 출력할 수도 있고, HTML 문서로 자동 변환할 수도 있는데 이 경우에도 테스트 슈트를 구성해 두면 많은 도움이 됩니다.

물론 GLib은 정교하게 테스트 슈트와 테스트 케이스, 픽스쳐를 구성할 수 있는 많은 API를 제공하지만, 복잡한 과정을 API 호출 하나로 처리할 수 있는 기능도 제공합니다.
g_test_add_func ("/onvif/nvc-connections", test_onvif_nvc_connections);

위 예제에서 g_test_add_func() 함수는 "onvif" 테스트 슈트 밑에 "nvc-connections" 이름의 테스트 케이스를 추가합니다. 테스트시 실행할 함수는 사용자가 직접 구현한 test_onvif_nvc_connections() 함수입니다. g_test_add_func() 함수가 테스트 슈트를 자동으로 생성해 주기 때문에 별도의 추가 작업이 불필요합니다. 비슷한 기능의 g_test_add_data_func() 함수는 테스트 함수에 데이터를 전달할 수 있어서, 한 함수로 데이터만 바꿔서 테스트하고자 할때 유용합니다. 하지만, 두 API는 픽스쳐를 지정할 수 없으므로, 픽스쳐를 사용하려면 g_test_add() 함수를 이용해야 합니다.일단, 간단한 예제 코드를 보여드리면 다음과 같습니다. ("Writing Unit Tests with GLib" 글에서 발췌했습니다)
#include <glib.h>

static void
simple_test_case (void)
{
/* a suitable test */
g_assert (g_bit_storage (1) == 1);

/* a test with verbose error message */
g_assert_cmpint (g_bit_storage (1), ==, 1);
}

int
main (int argc, char **argv)
{
/* initialize test program */
g_test_init (&argc, &argv, NULL);

/* hook up your test functions */
g_test_add_func ("/Simple Test Case", simple_test_case);

/* run tests from the suite */
return g_test_run ();
}

이 코드를 g-test-sample1.c 파일로 저장하고 컴파일 후 실행하면 다음과 같은 결과를 볼 수 있습니다.
$ gcc -o g-test-sample1 g-test-sample1.c `pkg-config --cflags --libs glib-2.0`
$ ./g-test-sample1
/Simple Test Case: OK

이 결과를 재활용하기 위해 XML 형식으로 저장하거나, HTML 문서로 만들고 싶다면 gtester / gtester-report 프로그램을 사용하면 됩니다.
$ gtester -o sample-log.xml g-test-sample1
TEST: g-test-sample1... (pid=2771)
PASS: g-test-sample1
$ gtester-report sample-log.xml > sample-log.html

위와 같이 실행하여 생성한 HTML 문서 결과는 다음과 같습니다.



참고로, gtester 프로그램의 인수로 여러 테스트 실행 파일을 한꺼번에 전달하면 모든 테스트 실행 파일의 테스트 슈트가 하나의 결과로 통합됩니다.

위 코드에서 사용한 테스트 코드를 보면 제일 먼저 g_test_init() 함수가 나타납니다. 이 함수는 테스트 기능을 초기화하는데, 리퍼런스 매뉴얼을 보시면 프로그램 실행 인수를 통해 사용자가 여러 테스트 옵션을 지정할 수 있는 걸 알 수 있습니다. 물론 특정 테스트 슈트만 실행하게 하는 옵션도 인수로 지정할 수 있습니다.

테스트 함수를 보면 g_assert_cmpint()라는 다소 생소한 API가 보이는데, GLib은 테스트 코드를 위해 이와 비슷한 매크로를 더 제공합니다.
#define g_assert             (expr)
#define g_assert_not_reached ()
#define g_assert_cmpstr (s1, cmp, s2)
#define g_assert_cmpint (n1, cmp, n2)
#define g_assert_cmpuint (n1, cmp, n2)
#define g_assert_cmphex (n1, cmp, n2)
#define g_assert_cmpfloat (n1,cmp,n2)
#define g_assert_no_error (err)
#define g_assert_error (err, dom, c)

위 매크로를 사용하여 테스트 코드를 작성하면 더 친절하고 자세한 에러 메시지를 출력합니다. 예를 들어 다음 코드는,
gchar *string = "foo"; g_assert_cmpstr (string, ==, "bar");

이런 메시지를 출력합니다.
ERROR: assertion failed (string == "bar"): ("foo" ==  "bar")

물론 기본적으로 실패한 경우에만 메시지를 보여줍니다.

그 외 더 많은...

지금까지 설명한 기본 기능 외에도 표준출력 / 표준에러 메시지를 표시하지 않도록 한 뒤 이 메시지에서 특정 문자열을 확인한다든가, 항상 동일한 패턴의 난수를 생성하여 이를 테스트에 이용하거나, 테스트에 시간이 얼마나 더 걸리는지 측정할 수도 있습니다. 프로그램을 종료시키는 치명적인 에러가 발생하는 경우도 테스트할 수 있고, 여러가지 테스트 모드(quick / slow / performace 등)를 두어 프로그램 인자를 이용해 원하는 테스트 코드만 실행할 수도 있습니다.

더 많은 활용 예제가 GLib 자체 테스트 코드에(glib/tests/testing.c) 있으므로, 별로 길지 않으니, 직접 확인해 보시기 바랍니다.

프로젝트에 활용하기

MVP 개발 모델과 TDD + 유닛테스트 도구를 이용하여 응용 프로그램을 개발하면(Presenter First 개발) 더 빠르고 쉽게 튼튼한 코드를 만들 수 있으니, 한 번 검토해 보시기 바랍니다. 개발자가 TDD 방법론을 주저하는 이유 중 하나가 테스트 코드까지 만들다 보니 늘어나는 코드량과 늘어나는 개발 시간 때문인데, 테스트 코드를 그대로 실제 코드로 재활용할 수 있다면 얘기가 달라지겠죠.

프로젝트 일일빌드시 테스트 루틴도 동작하도록 한뒤 자동으로 테스트 결과를 웹사이트에 게재하는 것도 좋은 개발 습관입니다. 아예 코드 수정 후 저장소에 커밋하면 반드시 모든 테스트 케이스를 통과해야만 커밋되도록 저장소를 설정할 수도 있지만, 엄청난 서버 부하를 야기할 수 있으므로, 테스트 케이스를 통과한 코드만 커밋할 수 있도록 가이드라인을 규정하는 것도 좋습니다.

유닛테스트는 특정 객체나 모듈의 모든 API가 항상 정상적으로 동작하는지를 검사하기 위해 사용합니다. 그래서 가장 기본적인 사용법은 공개 함수를 다양한 인수로 호출한 뒤 그 결과값을 확인하는 방식입니다. 하지만 실무에서는 그렇게 단순한(?) 버그만 존재하는게 아니라서, 특정 시나리오나 특정 조건을 만족할 경우에만 버그 현상이 재현되는 경우도 많습니다. 이러한 경우, 버그에 대한 테스트 케이스를 추가하고 이 케이스에 대한 테스트가 통과할때까지 디버깅을 합니다. 이렇게 해두면 동일한 버그가 나중에 재발하는 걸 방지할 수 있습니다. 대부분 회사에서는 버그(이슈)관리시스템을 사용하므로 g_test_bug() API를 사용하면 편리합니다.

참고로, GTK+ 라이브러리는 GLib 테스트 프레임워크를 기반으로 마우스 버튼 동작이나 키보드 입력을 에뮬레이션하는 기능처럼 GUI 프로그램 테스트용 API를 제공합니다. 더불어 Xvfb 같은 더미 X서버를 이용하면 원격 터미널이나 cron 작업처럼 실제 X서버가 없는 환경에서도 GUI 프로그램 테스트 진행이 가능합니다. 꼭 GTK+ 프로그램이 아니더라도, 폰트 렌더링 루틴이 정확한 그래픽 비트맵을 생성하는지, 특정 항목을 선택하고 특정 행동을 취했을때 정상적으로 문자열이 표시되는지 등도 테스트 케이스로 작성할 수 있습니다.

테스트 케이스 실행 방식 및 테스트 코드 위치

위 예제처럼 테스트 케이스를 특정 주제별로 나누어 각각의 실행파일로 만들어도 되지만, 테스트 케이스를 초기화하는 부분을 잘 정리하여 테스트 케이스를 여러 모듈로 분리한 뒤, 모든 테스트 케이스를 통째로 하나의 실행파일로 만들어도 됩니다. 이렇게 하면 추가적인 스크립트나 도구의 도움없이도 명령어 한번 실행으로 모든 테스트 케이스를 실행할 수 있기 때문에 더 편리할 수 있습니다. 또는 Clutter 프로젝트처럼 테스트 모듈을 각각 공유라이브러리로 만들어 플러그인처럼 로드해서 실행하는 방법도 있습니다.

하지만, 위 방식은 모두 실제 코드와 테스트 코드가 서로 다른 파일에 존재하는 방식입니다. 테스트 코드가 실제 코드와 하나의 파일에 존재한다면 테스트 코드 작성이 더 일상화되고 자연스러워질 수 있습니다. 그러므로, 프로그램 실행 파일 크기가 별로 문제가 되지 않는다면, 또는 릴리스 / 디버그 모드를 분리하여 컴파일하도록 구성된 프로젝트라면, 프로그램에 특정 옵션을 주었을 경우에만 테스트 케이스 실행 모드로 동작하게 하면 됩니다. 물론 특정 테스트 프로그램은 예제로 사용하기 위해 분리할 수도 있겠지만, 모듈이나 객체의 고유 기능만 테스트하는 코드라면 같은 파일에 있는게 더 자연스러울 수 있습니다. 예를 들어 GObject 객체라면, 속성(properties) / 시그널(signal) 이름이 갑자기 변경되었을때 이를 참조하는 모듈이 문제를 일으키지 않도록 하기 위해, 'validate-properties', 'validate-signals' 등의 테스트 케이스를 추가한뒤 통과하지 못했을 경우 g_test_message() 등을 이용해 이를 참조하는 모듈을 찾아 수정하라는 강조 메시지를 표시하는 것도 가능합니다. 또한 특정 시그널이 정상적으로 발생하는지, 순서대로 발생하는지 확인할 수 있습니다. 그리고 무엇보다도, 같은 파일에 있으면 내부 자료구조에도 접근할 수 있으므로 내부 로직에 대한 테스트 코드를 작성하는 것도 가능해집니다.

따라서 무조건 한 가지 방식만 고집하기보다, 적절하게 필요에 따라 알맞는 방식을 선택하는 것이 중요합니다.

결론

뭐 다른 결론이 있을리 없을만큼 유닛 테스트와 리그레션 테스트(regression test) 등은 이미 소프트웨어 개발 분야 전반에 광범위하게 사용하고 있습니다. 다만, C 언어를 이용해 개발하는 경우 리거시(legacy) 코드가 너무 많거나, 마땅한 테스트 도구를 찾지 못했거나, 여러가지 이유로 도입하지 못하는 경우가 많은데, 함께 잘 극복하고 익숙해져서 더 좋은 방향으로 나아가야 하지 않을까... 생각해 봅니다.

(이 글은 개인 블로그에 함께 게재되어 있습니다)

The Board 소개

힘겹게 겨우 다시 한 학기를 마무리했지만, 할 일은 여전히 밀려 있습니다. 그래서 잠시 머리도 식힐겸 요즘 관심있게 지켜보고 있는 GNOME 프로젝트 중 하나인 The Board 프로젝트에 대해 간략하게 소개해볼까 합니다.

일단, 백마디 말보다 스크린샷 하나가 더 좋을 것 같군요.


이 프로그램은 쉽게 말해 다음과 같은 오프라인 보드를 컴퓨터에서 흉내내는 것입니다. 다만 아직은 개인 자료 수집용입니다.


그놈 셸(GNOME Shell)과 마찬가지로, 클러터(Clutter) 라이브러리를 이용해 자바스크립트(JavaScript) 언어로 작성되었다는 점이 흥미롭습니다. 다음은 실제 동작 화면을 캡쳐한 동영상 모음입니다.






개발 로그를 들여다보면 웹브라우저에서 직접 접근할 수 있는 인터페이스가 추가되고 있는 것도 같은데, 언젠가는 현재 인터넷의 글목록 방식 게시판이 아닌 이와 같이 실생활을 모방한 방식의 인터넷 게시판도 등장하지 않을까... 상상해봅니다.

(이 글은 개인 블로그에 함께 게재되어 있습니다)

2011년 7월 6일 수요일

GNOME3에서 주목해야할 기술!


GNOME3가 공식 출시된지 두어달이 지났습니다. 우분투11.04에서 GNOME3 대신 Unity를 선택하는 바람에 우분투 사용자가 많은 국내에서는 GNOME3를 사용하는 사용자가 많지 않을 것으로 예상합니다. 대신 최신 Fedora를 설치하면 GNOME3를 사용할 수 있습니다.

GNOME3에서는 GNOME Shell이라는 Window Manager가 새롭게 개발되었고, 상당수의 코드가 GObject Introspection기술를 이용하여 JavaScript로 개발되었습니다. 이번 글에서 이러한 기술에 관하여 간단하게 소개해볼까 합니다.

GNOME Shell
GNOME Shell은 프로그램을 실행하고, 윈도를 전환시키고, 윈도가 전환될 때나 작업 환경이 변경될 때, 각종 효과를 만들어 냅니다. 기존 GNOME2의 데스크탑, 메뉴, Panel을 대치한다고 보면 됩니다.

http://live.gnome.org/GnomeShell/Technology
위 아키텍쳐를 보면 GNOME Shell은 크게 Windows Manager, Compositing Manger 역할을 하는 Mutter와 Shell Toolkit으로 구성된 것을 알 수 있습니다. Mutter는 기존 GNOME Desktop의 Window Manager인 MetaCity의 Core를 포하고 있습니다. 서로 다른 Process로 분리된 Window Manager와 Composting Manager를 하나로 프로그램으로 통합한것입니다. Clutter는 화면을 구성하는 UI와 Animation을 구현하기 위해 사용됩니다. 실제 Mutter와 Shell Toolkit은 GObject Introspection과 JavaScript Binding을 통해 JS로 사용이 가능합니다.

Clutter
Clutter는 GPU 가속을 지원하는 3D기반의 2D Animation 엔진입니다. 직접 OpenGL API를 사용하지 않아도 쉽게 3D 효과를 UI에 적용할 수 있습니다. Stage와 Actor라는 개념을 제공하여, 직관적으로 Animation을 구현하고, 각 Actor에 3D 효과를 적용할 수 있습니다.

Mutter
Mutter는 GNOME Desktop의 새로운 Window Manager입니다. Clutter로 Compositing Manager기능을 함께 구현하여, 사용자에게 Compositing Manager를 선택할 자유를 제한했다고 약간의 논란이 있었습니다. 즉, Compiz 사용자는 Mutter를 사용할 수 없는 것이이죠. 아마, 그 이유 때문에 우분투도 GNOME-Shell을 사용하지 않는 것으로 알고 있습니다.

GObject Introspection
GObject Introspection은 다양한 언어에서 GObject기반 library에 쉽게 binding할 수 있도록 지원합니다. 지금까지는 API가 변경되면 binding을 수정해야했지만, 이제는 더 이상 그럴 필요가 없습니다. 좀 더 자세한 내용은 지난 GNOME3 Launch Party에서 발표된 슬라이드 자료를 참고하세요.

위와 같이 GNOME3에 Window Manager가 변경되는 등 많은 변화가 있었고, Clutter, GObject Introspection과 같은 기술로 구현되어 있습니다. 각각의 기술 또한 다양하게 활용될 수 있으므로 자세하게 살펴볼 수 있는 기회를 갖도록 하겠습니다.

고맙습니다~

2011년 7월 3일 일요일

jhbuild 사용하기

새로운 소프트웨어를 빌드하고 실행해 보는 것은 상당히 흥미있는 일이지만, 기존 시스템이 변경되어 갑자기 데스크탑 환경이 실행되지 않으면 무척 난감합니다. 게다가 의존성이 있는 라이브러리까지 모두 새롭게 빌드하려면 시간과 노력이 많이 듭니다.

이 때, 필요한 것이 바로 jhbuild입니다. jhbuild는 사용하고 있는 시스템 환경에 영향을 주지 않고, 따로 빌드와 실행 환경을 구성해주고  자동으로 소스 코드를 바로 다운로드 받아, 바로 빌드하도록 도와주는 툴입니다. 의존성 있는 라이브러리도 다운로드 받아서 함께 빌드해주니 정말 편리합니다.

jhbuild의 좋은 점이 무엇일까?

1. 모듈 세트로 소스코드를 다운로드 받아 configure, build, install이 가능

$ jhbuild build gtk+ 

gtk+관련 모듈을 다운로드 받아서 알아서 빌드한 후, ~/.jhbuildrc에 설정한 위치로 빌드된 파일을 설치해준다. 

2. 원하는 모듈을 버전 별로도 빌드 및 설치 가능

3. 시스템이 깨긋한 상태에서 모듈과 의존성 있는 모듈을 새롭게 소스코드 부터 받아서 설치까지 검증

4. 자동화 빌드 시스템 구축

처음 jhbuild를 설치하고 주의할 점

.jhbuildrc 파일에 아래 두 줄을 추가해야 합니다.
addpath('PKG_CONFIG_PATH', os.path.join(os.sep, 'usr', 'lib', 'pkgconfig'))
addpath('PKG_CONFIG_PATH', os.path.join(os.sep, 'usr', 'share', 'pkgconfig'))

이렇게 해야, /usr/lib에 설치된 다른 의존성 있는 모듈을 사용하여 빌드가 가능합니다. jhbuild가 모든 의존성 있는 라이브러리를 다운로드 받아 빌드하지는 않기 때문입니다. 즉, png, tiff 라이브러리는 apt-get으로 직접 설치해야 합니다.

참고