23 Jul
2013
Posted in: 코드
By    8 Comments

boost 라이브러리 집중 분석


boost 라이브러리 집중 분석
by 신영진(YoungJin Shin), codewiz at gmail.com, @codemaru, http://www.jiniya.net

시쳇말로 C 언어는 2시간 배워서 20년 써먹고, C++은 20년 배워서 2시간 써먹는다는 이야기가 있다. 그만큼 C 언어는 간결하고 핵심적인 기능만 포함한 언어인 반면, C++은 복잡하고 다양한 기능이 포함돼 있다. 나는 C 언어를 배운지는 20년, C++은 13년 정도 됐다. 그간 주로 일했던 도메인이 보안이라 성능 이슈가 주요하고 네이티브 코드를 많이 작성해야 했기에 주력 프로그래밍 언어는 언제나 C++이었다. 그렇게 오래 함께한 언어임에도 C++은 여전히 어려운 언어이고, 지금까지도 항상 공부해야 하는 언어다. 물론 이런 현상은 단지 내가 멍청하기 때문만은 아닌 것 같다. 실제로 C 언어의 창시자인 Brian Kernighan 할아버지도 그런 의견을 피력했던 적이 있었기 때문이다.

때때로 나는 C 대신 C++을 사용해서 프로그램을 작성합니다. 제가 생각하기에 C++은 모든 기능을 내장하기 때문이라고 하더라도 매우 큰 언어입니다. 어떤 크기의 C 프로그램을 작성하든지 저는 75, 80, 90% 정도의 언어 기능을 사용합니다. 다시 말하면 어떤 종류의 프로그램이든 간에 대부분의 C 언어 기능이 유용하다는 점 입니다. 이와는 대조적으로 제가 C++을 사용한다면 언어의 10% 정도도 사용하지 못합니다. 그리고 나머지 90%는 제가 이해하고 있다고 생각하지 않습니다. 이러한 점들에서 전 C++은 너무 크다고 말합니다. 그러나 C++은 큰 프로그램을 작성하는데 필요한 많은 기능들을 제공합니다. 객체를 만드는 것이 가능하며, 내부적인 정보의 표현을 보호할 수 있습니다. 그래서 결국 내부를 보지 못하게 만드는 훌륭한 외관을 표현할 수 있습니다. C++은 제가 생각하기에 아주 방대한 양의 유용한 메카니즘을 가지고 있습니다. 그리고 그것은 C 언어가 당신에게 주지 못하는 점 입니다.

Brian Kernighan, 2000

기존의 C++이란 언어 자체도 방대한 기능으로 무장한 언어였지만 모던 C++로 불리는 C++11과 C++13, C++14로 불리며 지금 계속 개정 작업이 진행되고 있는 신규 표준은 훨씬 더 방대한 내용을 담고 있다. 상당히 최신 기술들이기 때문에 아직까지도 골수 C++ 프로그래머들도 이러한 기능을 잘 모르는 경우가 많다. 기능을 지원하는 컴파일러들이 제한적이기 때문에 실용성이 없다고 주장하는 입장도 많이 있다. boost는 이런 간극을 메워줄 수 있는 주요한 라이브러리다. 표준에 채택된 기능을 상당수 포함하고 있고, 추후 표준에도 적극적으로 라이브러리 기능들이 제안되고 있기 때문이다. 더불어 C++11을 지원하는 새로운 컴파일러를 사용하지 않더라도 boost를 사용하면 새로운 표준 라이브러리 기능들을 사용할 수 있기 때문에 더욱 효과적이다.

왜 boost인가?
boost의 주요 기능을 소개하기에 앞서서 왜 이런 복잡한 형태의 라이브러리를 사용해야 하는지에 대해서 먼저 알아보는 것이 중요한 것 같다. 왜냐하면 인식이 조금씩 바뀌고 있지만 여전히 boost와 같은 형태의 복잡하고 거대한 라이브러리를 사용하는 것에 대해서 회의적인 프로그래머들이 상당수 있기 때문이기도 하고, 무엇을 공부함에 있어서 그 필요성을 절감하고 공부하는 것과 단지 다른 사람들이 쓴다고 하니까 배우는 것에는 큰 차이가 있기 때문이기도 하다.

반대하는 쪽 주장의 면면을 살펴보면 이렇다. 1) 버그가 있다. 2) 너무 복잡하다. 3) 내가 만드는 걸 선호한다.

이 중에서 버그 이야기를 먼저 해보자. 결론만 먼저 말하면 사실 이런 주장을 하는 프로그래머치고 boost의 버그를 정확하고 명백하게 보여주는 프로그래머는 없었다. 대부분 그들이 도입하면서 겪었던 일부 문제들을 가지고 boost의 버그로 말하는 경향이 있는데 여기에는 boost 라이브러리의 사용법을 명확하게 몰라서 생긴 문제가 거의 대다수였다. 즉, 돌려 말하면 윈도우 API를 잘못 사용해서 잘못된 결과를 얻어놓고서는 윈도우 API의 버그라고 말하는 경우와 동일한 경우가 대부분이라는 점이다. 백 번 양보해서 제대로 된 버그를 발견했다고 치더라도 그걸 고쳐서 오픈소스 커뮤니티에 기여하면 된다. boost와 같은 라이브러리의 경우 전세계 수많은 개발자들이 지켜보고 있기 때문에 그 어떤 프로젝트보다도 결함이 신속하고 빠르게 고쳐진다. 적어도 버그 문제에 있어서는 직접 작성하는 것보다 훨씬 고품질의 결과를 얻을 수 있다는 점은 인정하자.

너무 복잡하다. 맞다. boost 라이브러리는 영문 정식 명칭이 library가 아닌 libraries로 돼 있을 만큼 정말 엄청나게 다양한 라이브러리의 묶음이다. 걔 중에는 유용한 것들도 있지만 당연히 사용하는 쪽 입장에서는 불필요한 부분도 많이 있다. 하지만 이는 다시 생각해보면 복잡해서 잘 모르는 부분은 사용하지 않으면 그만이다. boost를 사용한다고 해서 거기 있는 모든 라이브러리를 몽땅 써야 하는 것은 아니다. 더욱이 boost의 경우에 템플릿 기반의 라이브러리라 대다수 코드들은 헤더를 포함하지 않으면 그 어떤 코드도 우리 프로젝트에 영향을 미치지 않는다.

끝으로 꼭 직접 만든 것만 믿는 프로그래머들이 있다. 하지만 이건 정말이지 바보 같은 생각이다. 왜냐하면 조금만 시각을 달리하면 직접 만든 건 결국 하나도 없기 때문에다. 심지어 C++이란 언어 자체도 직접 만든 것은 아니다. 컴파일러도 마찬가지다. 그러니 직접 만든 것만 쓰겠다는 생각은 자신의 시야를 너무 좁은 곳에 두는 관점이라는 걸 명심하자.

자 그럼 이제 반박을 떠나서 boost와 같은 외부 라이브러리 사용의 긍정적인 측면을 살펴보도록 하자. 버그의 정반대 측면인 안정성을 들 수 있다. boost와 같은 검증된 라이브러리는 수많은 프로그램에서 사용되고, 전세계에 분포한 다양한 개발자들이 프로젝트에 참여하고 있다. 보는 눈이 많은 만큼 훨씬 더 안정적일 수 밖에 없다. 혹여 사용하다가 결정적인 버그를 발견했다손 치더라도 오픈 소스이기 때문에 수정해서 사용할 수 있고, 커뮤니티에 기여하는 경우에는 다음부터는 유사한 문제를 겪는 프로그래머가 없도록 도와 줄 수도 있다.

다음은 멀티 플랫폼이다. 요즘 컴퓨팅 환경의 트렌드를 한 마디로 요약하자면 멀티 플랫폼이다. 컴퓨터가 들어가지 않은 디바이스를 찾기 힘들 지경이다. 자동차, 세탁기, 냉장고, TV, 핸드폰, 손목시계, 안경 등 우리가 상상할 수 있는 모든 곳에는 컴퓨터가 탑재되고 있다. 그만큼 프로그래머들은 고달픈 시대다. 한가지 플랫폼만 했어도 충분했는데 이제는 이쪽 저쪽을 다 신경 써야 하기 때문이다. 이럴 때 가장 좋은 것이 이식성이 있는 소스 코드다. boost의 경우 다양한 환경에 대한 추상화 계층을 사용하기 때문에 boost::asio와 같은 비동기 입출력 라이브러리를 사용한다면 우리는 Windows, BSD, Linux 등과 같은 플랫폼에 상관 없이 그 플랫폼에 얼추 최적화된 구현을 바로 사용할 수 있다.

끝으로 가장 결정적인 이유는 좋은 라이브러리를 써봐야 좋은 라이브러리를 만들 수 있기 때문이다. 컴퓨터 공학을 가르치는 대학의 경우 대부분 소스 코드 베끼기가 실력 향상에 좋지 않다는 이유로 소스 코드 보는 행위를 장려하지 않고 있다. 안타깝게도 이런 현상 때문에 제대로 된 코드를 쓸 줄 아는 프로그래머가 그렇게도 드문 것 같다. 다른 사람이 만든 소스 코드, 또는 선배 프로그래머들이 만든 소스 코드를 봐야지 아 이런 식으로 짜는 구나를 알 수 있다. 파서를 strstr로 짜고 있는 프로그래머, 함수 포인터를 몰라서 switch/case를 일일이 열거하고 있는 프로그래머를 보면 정말이지 어디서부터 이야기를 해야 할지 답이 안 나오는 지경이다. 직접 만들더라도 많은 사람들이 사용하는 좋은 구조에 대해서는 알아두는 것이 도움이 된다는 점을 꼭 기억하자.

shared_ptr
아마도 boost 라이브러리를 접했던 프로그래머의 절반 이상이 shared_ptr 이라는 스마트 포인터 하나를 사용하기 위해서 접했다고 해도 과언이 아닐 만큼 boost 보다 더 유명한 클래스다. 이제는 C++11에 포함돼 있기 때문에 std::shared_ptr 형태로 사용할 수 있다. 하지만 아직 C++11을 지원하는 컴파일러를 사용할 수 없는 환경이라면 boost는 여전히 강력한 대안이다.

C++11 이전의 표준에 포함됐던 유일한 스마트 포인터 클래스는 auto_ptr 클래스였다. auto_ptr의 경우 소유권 이전에 대해서 다소 난해한 정책을 가지고 있어서 스마트 포인터를 처음 접하는 프로그래머에게 좋지 않은 인상을 남기는 허들 역할을 톡톡히 했다. <리스트 1>에는 전형적인 auto_ptr의 잘못된 사용을 보여주고 있다. auto_ptr의 경우에는 복사 시에 소유권이 이전되는 데 이게 상황에 따라서 상당히 미묘한 많은 문제를 일으킨다. 더욱이 이런 특징 때문에 결정적으로 STL 컨테이너와는 전혀 연동을 할 수 없기 때문에 반쪽 짜리 스마트 포인터라는 오명을 가지기도 했었다.

리스트 1 auto_ptr의 소유권 이전 문제를 보여주는 전형적인 예제

#include "memory"
#include "iostream"

using namespace std;

class Value
{
public:
    int val_;
    Value()
    {
        cout << "Value ctor" << endl;
    }

    ~Value()
    {
        cout << "Value dtor" << endl;
    }
};

void fn(auto_ptr<Value> param)
{
    param->val_ += 1;
}

int _tmain(int argc, _TCHAR* argv[])
{
    auto_ptr<Value> pa(new Value);
    pa->val_ = 3;
    cout << "pa val_ = " << pa->val_ << endl;
    cout << "fn 호출 시작" << endl;
    fn(pa);
    cout << "fn 호출 끝" << endl;
    cout << "pa val_ = " << pa->val_ << endl;
    
    return 0;
}

auto_ptr의 소유권 이전 문제를 개선한 것이 boost의 shared_ptr이다. shared_ptr은 내부적으로 레퍼런스 카운팅을 사용하기 때문에 복사하더라도 해당 객체를 참조하는 포인터가 모두 사라지는 경우에만 실질적으로 객체 파괴 작업이 일어난다. 복사 시에 레퍼런스 카운트를 증가시키고, 파괴 시에는 레퍼런스 카운트를 감소시켜 0이 되는 경우에 실제로 객체를 파괴하도록 만들어져 있기 때문이다. 이런 장점 때문에 auto_ptr과 달리 STL 컨테이너에 넣어도 안전하다.

shared_ptr은 레퍼런스 카운팅이라는 기본적인 장점에 추가적으로 커스텀 파괴자를 지원하기 때문에 스마트 포인터의 활용도를 한층 더 높일 수 있다는 장점도 있다. <리스트 2>에는 세 개의 파일 리소스를 사용해서 처리를 하는 일반적인 루틴을 보여주고 있다. 이 경우에 사용하는 리소스가 추가될 때마다 인덴트 깊이가 깊어져서 코드가 보기 힘들어지는 경향이 있다. 실제로 스마트 포인터가 없는 COM 코드에서는 이러한 코드가 일반적이다.

<리스트 2>의 코드를 가독성이 좋도록 개선한 버전이 <리스트 3>에 나와 있다. 변경된 코드에서는 실패 시에 바로 함수 실행을 리턴하도록 만들었다. 하지만 이렇게 할 때에도 매 실패 시점마다 프로그래머가 현재 획득한 리소스와 해제해야 할 리소스 목록을 알고 있어야 한다는 단점이 있다.

이런 코드의 경우 가장 깔끔한 해결 방법은 기존의 레거시 파일 처리 루틴을 사용하지 않고 RAII(Resource Acquisition Is Initialization) 형태로 설계된 파일 클래스를 사용하는 것이 정답이다. 하지만 현실 세계에 존재하는 수많은 레거시 코드를 일일이 그런 형태로 래핑하는 것은 너무 이상적인 이야기일 수 밖에 없다. 이런 경우에 중간 단계의 절충안으로 shared_ptr을 사용할 수 있다. <리스트 4>에는 shared_ptr의 커스텀 파괴자를 사용해서 자원을 관리하는 코드가 나와 있다. shared_ptr이 파괴될 때 fclose가 호출되기 때문에 파일 포인터 자원이 자동적으로 소거된다. 이 코드에서 프로그래머는 자원 생성 시에만 파괴 루틴을 등록하고 이후에는 해당 자원에 대해서는 신경 쓰지 않아도 되기 때문에 실수할 가능성이 줄어들고 그만큼 자원 관련 버그도 줄여준다. 예외 발생 시에도 자원을 안전하게 파괴할 수 있다는 것은 추가적인 장점이다.

리스트 2 파일 포인터 자원 3개를 사용하는 일반적인 코드

void
MergeFile(LPCSTR dst, LPCSTR src1, LPCSTR src2)
{
    FILE *dfp;
    FILE *sfp1;
    FILE *sfp2;

    dfp = fopen(dst, "wb");
    if(dfp)
    {
        sfp1 = fopen(src1, "rb");
        if(sfp1)
        {
            sfp2 = fopen(src2, "rb");
            if(sfp2)
            {
                // 실제 처리할 작업

                fclose(sfp2);
            }

            fclose(sfp1);
        }

        fclose(dfp);
    }
}

리스트 3 실패시 리턴 방식을 사용한 코드

void
MergeFile2(LPCSTR dst, LPCSTR src1, LPCSTR src2)
{
    FILE *dfp;
    FILE *sfp1;
    FILE *sfp2;

    dfp = fopen(dst, "wb");
    if(!dfp)
        return;

    sfp1 = fopen(src1, "rb");
    if(!sfp1)
    {
        fclose(dfp);
        return;
    }

    sfp2 = fopen(src2, "rb");
    if(!sfp2)
    {
        fclose(sfp1);
        fclose(dfp);
        return;
    }

    // 실제 처리할 작업

    fclose(sfp2);
    fclose(sfp1);
    fclose(dfp);
}

리스트 4 shared_ptr을 사용하여 자원 관리를 하는 코드

#include "boost/shared_ptr.hpp"

typedef boost::shared_pt<void> vsptr;

void
MergeFile3(LPCSTR dst, LPCSTR src1, LPCSTR src2)
{
    FILE *dfp;
    FILE *sfp1;
    FILE *sfp2;

    dfp = fopen(dst, "wb");
    if(!dfp)
        return;

    vsptr dfp_closer(dfp, fclose);

    sfp1 = fopen(src1, "rb");
    if(!sfp1)
        return;

    vsptr sfp1_closer(sfp1, fclose);

    sfp2 = fopen(src2, "rb");
    if(!sfp2)
        return;

    vsptr sfp2_closer(sfp2, fclose);

    // 실제 처리할 작업

}

bind
boost에서 shared_ptr 다음으로 유명한 것을 들자면 단연코 bind가 될 것이다. 유연하게 설계된 클래스가 얼마나 효율적으로 기존 시스템과 마찰 없이 잘 결합될 수 있는지를 단적으로 보여주는 클래스라고 할 수 있다.

bind은 기본적으로 함수 호출 객체를 만들어주는 역할을 한다. <리스트 5>에는 기본적인 사용 방법이 나와 있다. fn이라는 기존 함수에 a, b, c, d, e 파라미터로 1, 2, 3, 4, 5를 전달해서 호출하는 함수 객체를 만드는 것을 보여주고 있다. 해당 함수 객체를 호출하면 결과 값은 15가 출력된다.

<리스트 6>에는 플레이스홀더를 사용해서 bind 객체를 생성하는 것을 보여준다. _1로 표시된 자리에 해당 함수 객체를 호출할 첫 번째 파라미터가 들어간다는 의미다. 따라서 이 함수 객체를 호출할 때에는 _1 자리에 들어갈 파라미터를 넣어주어야 한다. 예제에서는 10을 넣어주었다. 따라서 이 객체는 최종적으로 fn(1, 2, 10, 4, 5)를 호출하는 결과를 보여준다.

<리스트 7>에는 플레이스홀더를 두 개 사용한 예제가 나와 있다. _1에는 첫번째 파라미터가, _2에는 두번째 파라미터가 들어간다. 따라서 이 함수 객체를 사용할 때에는 2개의 파라미터를 넣어주어야 정상적으로 호출된다. 예제 코드의 bind는 최종적으로 fn(1, 11, 10, 4, 5)를 호출하는 것과 동일한 기능을 한다.

리스트 5 기본적인 bind 예제

#include "boost/bind.hpp"

int fn(int a, int b, int c, int d, int e)
{
    printf("a = %d, b = %d, c = %d, d = %d, e = %d\n", a, b, c, d , e);
    return a + b + c + d + e;
}

int main()
{
    printf("%d\n", boost::bind(fn, 1, 2, 3, 4, 5)());
    return 0;
}

리스트 6 bind 플레이스홀더 사용 예제 1

int main()
{
    printf("%d\n", boost::bind(fn, 1, 2, _1, 4, 5)(10));
    return 0;
}

리스트 7 bind 플레이스홀더 사용 예제 2

int main()
{
    printf("%d\n", boost::bind(fn, 1, _2, _1, 4, 5)(10, 11));
    return 0;
}

boost::bind의 경우 클래스 멤버 함수에도 동일하게 적용할 수 있다. <리스트 8>에는 멤버 함수에 bind를 적용한 코드가 나와 있다. 이 코드에 나와 있는 두 bind 호출은 모두 최종적으로 bob.Say(“Hello”)를 호출하는 역할을 한다. boost::bind는 파라미터를 지정하는 경우 값을 복사하는 형태로 동작하기 때문에 내부 동작을 보여주기 위해서 생성자, 복사 생성자, 소멸자에 각각 printf를 추가해 놓았다.. 출력된 내용을 살펴보면 첫 번째 bind의 경우에는 지속적으로 객체가 복사되고 있음을 확인할 수 있다. 이를 제거하기 위해서는 boost::ref를 사용하면 된다. 첫 번째 bind 코드를 boost::bind(&Man::Say, boost::ref(bob), “Hello”)()와 같이 고쳐서 테스트를 해보면 값이 복사되지 않는 것을 확인할 수 있다.

리스트 8 멤버 함수에 bind 적용 예제

#include "stdio.h"
#include "boost/bind.hpp"

class Man
{
public:
    std::string name_;

    Man(const char *name)
    {
        name_ = name;
        printf("%s ctor\n", name_.c_str());
    }

    Man(const Man &r)
    {
        name_ = r.name_;
        printf("%s cctor\n", name_.c_str());
    }

    ~Man()
    {
        printf("%s dtor\n", name_.c_str());
    }

    void Say(const char *msg)
    {
        printf("%s: %s\n", name_.c_str(), msg);
    }
};


int main()
{
    Man bob("Bob");
    boost::bind(&Man::Say, bob, "Hello")();
    boost::bind(&Man::Say, _1, "Hello")(bob);
    return 0;
}

bind의 객체가 복사된다는 특징은 shared_ptr과 같은 스마트 포인터와 결합하면 좀 더 유연한 동작을 가능하게 한다. <리스트 9>에는 shared_ptr 객체를 참조하는 bind 예제가 나와 있다. bind가 객체를 복사해 두었기 때문에 bob.reset 이후에도 객체가 파괴되지 않고 살아있어서 정상적으로 멤버 함수 호출이 이루어진다. 실질적인 bob 객체는 bind 함수 객체를 담고 있는 function 객체가 사라질 때 같이 소멸된다.

<리스트 9>의 코드에서 객체를 복사하지 않고 boost::ref를 사용해서 직접 참조했다면 bob.reset 시점에 객체가 사라지고 이후 호출되는 fn(“Hello”)는 사라진 객체를 참조하게 되기 때문에 크래시가 발생하는 문제점이 생긴다.

리스트 9 shared_ptr 객체의 멤버 함수를 참조하는 bind 예제

#include "stdio.h"
#include "boost/shared_ptr.hpp"
#include "boost/bind.hpp"
#include "boost/function.hpp"

using namespace boost;

int main()
{
    shared_ptr<Man> bob(new Man("Bob"));
    function< void (const char *) > fn =bind(&Man::Say, bob, _1);
    bob.reset();
    fn("Hello");
    return 0;
}

<리스트 9>의 코드에도 잠깐 등장하지만 bind 결과 함수 객체를 저장해놓고 함수 포인터와 같이 지속적으로 사용하기 위해서는 boost::function 클래스를 사용하면 된다. function 클래스의 템플릿 파라미터로 함수 원형을 넣어주면 된다. <리스트 9>의 예제 코드의 fn은 반환값이 없으며(void), 파라미터로 문자열을(const char *) 가지는 함수 객체를 만들겠다는 것을 의미한다.

C++에 익숙하지 않은 프로그래머의 경우에는 여기까지만 이야기하면 이걸 도대체 어디에다 사용하겠다는 건지 의아해 하는 경우가 많다. 그런 경우라면 <리스트 10>에 나와 있는 것과 같은 코드가 도움이 된다. 예제 코드에서는 벡터 멤버를 출력하기 위해서 이터레이터를 만들고 printf를 사용해서 벡터 멤버를 출력하고 있다. 하지만 이를 boost::bind를 사용하면 <리스트 11>에 나와 있는 것과 같이 간단하게 한 줄로 해결할 수 있다. 코드가 짧다는 것을 실수할 가능성은 줄어들고, 가독성은 좋아진다는 것을 의미한다.

리스트 10 이터레이터를 사용해서 벡터 멤버를 출력하는 코드

#include "stdio.h"
#include "vector"

using namespace std;

int main()
{
    vector<int> ints;
    ints.push_back(1);
    ints.push_back(2);
    ints.push_back(3);
    ints.push_back(4);

    vector::iterator it = ints.begin();
    vector::iterator end = ints.end();
    while(it != end)
    {
        printf("%d\n", *it);
        ++it;
    }

    return 0;
}

리스트 11 boost::bind를 사용해서 벡터 멤버를 출력하는 코드

#include "stdio.h"
#include "vector"
#include "algorithm"
#include "boost/bind.hpp"

using namespace std;
using namespace boost;

int main()
{
    vector<int> ints;
    ints.push_back(1);
    ints.push_back(2);
    ints.push_back(3);
    ints.push_back(4);

    for_each(ints.begin(), ints.end(), bind(printf, "%d\n", _1));

    return 0;
}

thread
멀티스레드란 환경이 이제는 표준적인 프로그래밍 환경으로 자리잡은 것 같다. 하지만 스레드를 플랫폼마다 지원하는 방식의 차이가 있어서 표준적인 방식으로 사용하기에는 아직 문제가 많다. 이런 경우에는 boost::thread를 사용하면 손쉽게 멀티 플랫폼을 지원하는 스레드 코드를 작성할 수 있다.

<리스트 12>에는 간단한 스레드 작성 코드가 나와 있다. 기존의 윈도우 스레드 구조와 크게 다르지 않은 코드다. boost::thread 생성자로 스레드 함수 포인터와 파라미터를 전달해서 스레드를 생성시키고 있는 것을 볼 수 있다.

윈도우 프로그래밍을 하면 게시판에 올라오는 흔한 질문 중에 하나가 클래스 멤버 함수를 스레드로 구동시키는 방법에 관한 것이다. 컨텍스트에 관한 개념이 없어서 하는 질문인데 기존 윈도우 코드로만 해결하려면 정적 메소드를 만들고 파라미터로 객체 인스턴스를 전달하는 복잡한 과정을 거쳐야 한다. 하지만 boost에서는 boost::bind를 이용하면 손쉽게 클래스 멤버 함수도 스레드로 호출할 수 있다. <리스트 13>에는 boost::bind를 사용해서 클래스 멤버 함수를 스레드로 호출하는 예제가 나와 있다.

리스트 12 boost::thread 예제

#include "stdio.h"
#include "boost/thread.hpp"

void ThreadProc(const char *msg)
{
    printf("%s\n", msg);
}

int main()
{
    boost::thread th1(ThreadProc, "Mark");
    boost::thread th2(ThreadProc, "Bob");

    th1.join();
    th2.join();
    return 0;
}

리스트 13 클래스 멤버 함수를 스레드로 호출하는 예제

#include "stdio.h"
#include "string"
#include "boost/thread.hpp"
#include "boost/bind.hpp"
#include 

class Man
{
public:
    std::string name_;

    Man(const char *name)
    {
        name_ = name;
    }

    void Say(const char *msg)
    {
        printf("[%d] %s: %s\n", GetCurrentThreadId(), name_.c_str(), msg);
    }
};

int main()
{
    Man bob("Bob");
    Man mark("Mark");
    
    boost::thread th1(boost::bind(&Man::Say, bob, "Hello"));
    boost::thread th2(boost::bind(&Man::Say, mark, "Bye~"));

    th1.join();
    th2.join();
    return 0;
}

언어를 넘어서
최근에는 C/C++의 영향력이 급격하게 줄어들고 있는 모양새다. 다양한 요인이 있겠지만 아무래도 첫 번째 요인은 컴퓨팅 파워가 놀랍도록 좋아진 점을 들 수 있을 것 같다. 이제는 더 이상 몇 바이트, 몇 나노초를 계산해가면서 프로그래밍하지 않아도 되는 세상이다. 또 다른 요인으로는 과거와는 다르게 프로그램에 기대하는 요구사항이 폭발적으로 증가했다는 점도 들 수 있다. 고객의 요구사항은 시시각각 변하고, 살아남는 프로그램이 되기 위해서는 그러한 요구사항의 변화를 빠른 속도로 대처해야 한다. 그런 점에서 정적인 언어인 C++은 다소 불리할 수 밖에 없었다.

모던 C++은 이러한 생산성에 관한 취약점을 극복하기 위해서 다양한 기능들을 추가했다. boost 라이브러리 같은 것들을 사용하면 기존 C++ 컴파일러를 사용해서도 제한적이나마 편리한 신규 기능들을 체험해 볼 수 있다. 하지만 이 모든 사실 이전에 반드시 기억해야 할 것은 C++이 항상 우리에게 주어진 문제를 푸는 최선의 도구는 아니라는 점이다.

C++ 언어를 사용하는 프로그래머의 경우 포인터를 다룬다는 점에서 나름 자부심이 많이 있는 편이다. 그래서 그런지 언어에 대한 자부심도 남다른 편인 것 같다. 하지만 모든 문제를 C++로 풀 필요는 없다. 주어진 문제를 해결하는데 적합한 도구를 사용하면 된다. C++로도 풀 수 있는 것이지 다른 대안 언어만큼 쉽게 풀 수 있는 것은 절대로 아니다. 그러니 C++ 언어의 새로운 기능을 꾸준히 탐색하는 것만큼이나 C++이란 언어의 도그마에 빠지지 않도록 각별히 주의해야 할 것 같다. 세상에 주어진 다양한 문제만큼이나 그 문제들을 독창적으로 해결할 수 있는 언어도 많다. 우리는 단지 적절한 도구를 사용해서 그 문제들을 풀기만 하면 되는 것이다.

boost 기능에 대해서 정리가 잘 된 슬라이드~ 백문이 불여일슬라이드 ㅋ~

Browser does not supports flash movie

  • 트랙백 주소: http://www.jiniya.net/wp/archives/11769/trackback

관련 글

  • http://dol9.tistory.com 오산돌구

    요새 C++로된 오픈 소스 봐야되서 학부때 C++ 배운 감으로 진행 하려했는데…..두둥
    부스트나오면서 멘붕왔습니다.
    이런 재미진 포스트를 봐서 정말 감사합니다. 예제 연습하고 다시 봐야겠어요 : )

  • http://archwin.net 아크몬드

    언어의 특성을 이해하는 수준으로 가려면 은근 오랫동안 수행(?)을 해야 하는 것 같습니다. 재밌게 읽었습니다.

  • zoops

    나도 잼있게 읽었어요. ㅎㅎㅎ
    저도 요새는 C# 이나 java 를 더 많이 쓰고있지만….
    개인적으로는 C나 C++ 이 프로그램 맛은 있죠. ㅋ

    영진씨가 이야기한 F# 은 아직 듣기만 하고 본적은 없어여…
    한번 소개하는 포스팅 기대해볼께여. ㅎㅎㅎ

  • codewiz

    오신돌구 // 연습이 중요하죠. 오픈 소스 분석이 잘 되셨으면 합니다 ㅋ~

    아크몬드 // 네. 맞는 말씀입니다. 수행(?!)이 중요하죠.

    zoops // 형, F# 101을 업데이트 했습니다 ㅋㅋㅋ~ 저도 아직 들은 수준 정도지요 뭐 ㅋ~

  • http://www.mint64os.pe.kr kkamagui

    재미있는 글 잘 봤습니다. ^^
    C++은 점점 덩치가 커지고 있어서 이제는 따라가기도 어려울 것 같네요 ㅠㅠ

    마지막에 있는 재미있는 PPT도 잘 봤습니다. ^^
    ASIO는 한 번 써먹어 볼 만하군요. ^^)/~

  • 책읽는잉여

    좋은 내용 잘 읽었습니다. :)

  • codewiz

    책읽는잉여 // 감사합니다. ^^;;

  • http://blog.naver.com/cestlavie_01 cobus

    좋은 정보 감사합니다. 정규식 때문에 boost를 알아보다가 여기까지왔네요. ㅎ