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) 메카니즘을 사용하기 때문에 이론적으로는 거의 모든 파일 디스크립터를 사용할 수 있습니다. 물론 비슷한 방식으로 윈도우 운영체제에서 이벤트 핸들이나 소켓 핸들도 사용할 수도 있습니다.

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

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