고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #13 - 로깅 라이브러리 빌드하기

Posted at 2007.09.04 09:56 // in S/W개발/고절가주팁 // by 김윤수


(약간 Update된 버전입니다. 첨부파일도 함께 Update했습니다)

고절가주팁 열세번째 시간입니다. 지난글을 통해 로깅 라이브러리를 Open Source 로 공개한다는 말씀을 드렸습니다. 소스를 공개한 이후 몇 분으로부터 빌드 관련 에러를 보고 받은 게 있어서 그 문제에 대해 말씀드리고, 제가 만든 빌드 파일을 공개하려고 합니다(로깅 라이브러리 관련된 글을 처음 보시는 분들은 본문에 나와 있는 로깅 라이브러리를 클릭해 보시면 이전 글들을 모두 보실 수 있습니다).

저는 로깅 라이브러리 관련 글을 작성하면서 계속해서 Microsoft Visual C++ 2005 Express 를 사용해서 다른 환경에서 빌드되는지를 확인하지 않았더니 아니나 다를까 에러가 나더군요. 제가 사용했던 건 C++ 표준 라이브러리에만 있는 것들을 사용했었기 때문에 에러가 나면 안되는 거지만 세상만사가 우리 뜻대로만 안되는 것 아니겠습니까? 표준을 따르지 않는 개발 환경이 있기 마련이지요. 게다가 첫번째 C++ 표준이 제정된 것이 다른 언어에 비해서는 짧은 역사를 가지고 있어서 아직까지도 표준을 제대로 지원하지 못하는 컴파일러들이 당연히 있을 거라는 생각이 드네요.

우선 첫번째 빌드 문제!

Visual Studio 6 에서 빌드하면 다음과 같은 에러가 발생하더군요.

c:\documents and settings\김윤수\my documents\내 작업\yslog\logsource.cpp(49) : error C2784: 'class std::mem_fun_t<_R,_Ty> __cdecl std::mem_fun(_R (__thiscall _Ty::*)(void))' : could not deduce template argument for '<Unknown>' from 'bool (__thiscal
l YSLog::LogListener::*)(class YSLog::LoggingEvent *)'


역시나 눈이 훽훽 돌아가는 에러 메시지입니다. 뭔 에러 메시지를 이렇게나 해석하기 어렵게 출력해 대는지... 제가 컴파일러 만든다면 절대 저런식으로 메시지 출력하지 않을텐데 말입니다(헉! 컴파일러 만든 분 한테서 돌 날라오는 군요!!)

에러가 발생했을 때 가장 먼저 확인해야할 건 무엇일까요 ? 자~ 문제 들어갑니다. 1) 코드, 2) 매뉴얼, 3) 컴파일러 버전, 4) 에러 메시지. 시간은 10초 드리겠습니다. 일, 이, 삼, 사, 오, 육, 칠, 팔, 구, 십. 삐~~~~~~~~ 답은 바로 4번입니다. 가장 먼저 에러 메시지가 뭔지 확인해 봐야겠죠. 개발자들이 개발 중에 문제점을 만나면 보통은 에러 메시지를 보지도 않고, 바로 코드로 가더군요. 그러면서 코드를 한참 들여다 보고 나서 이상하다 잘못된 게 없는데...? 하면 반문하는 걸 여러번 봤습니다. 그걸 보면서 저는 "에러 메시지에 소스코드명이랑 라인번호 있네. 그 라인에 뭐뭐뭐뭐가 잘못됐데. 좀 확인해 봐." 그럼 개발자는 바로 그곳을 확인해 보고는 대부분의 경우 문제를 발견하고서는 "어~ 이거 잘못됐네. 어떻게 아셨어요 ?" "에러 메시지에 나와 있잖아~" 종종 이런 웃기지도 않은 대화를 하곤 했답니다. 여러분은 이렇게 하지 않으시겠죠 ?

자~ 그럼 LogSource.cpp 의 49번째 라인으로 가보겠습니다. Visual Studio 6 에서는 에러 메시지를 더블 클릭하면 바로 그 라인으로 갑니다. 다음과 같은 코드가 나올 겁니다.

46: bool LogSource::unconditionalLog(LoggingEvent* pLogEv) const
47: {
48:   for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
49:     bind2nd(mem_fun(&LogListener::publish), pLogEv));
50:
51:   return true;
52: }

위에 파랗게 표시한 부분이 뭐가 잘못됐을까요 ? 에러 메시지를 다시 찬찬히 살펴 보실까요 ? (설명의 편의를 위해 약간 간략화 시켰습니다)

'class mem_fun_t<_R,_Ty> mem_fun(_R (_Ty::*)(void))' : could not deduce template argument for '<Unknown>' from 'bool (YSLog::LogListener::*)(class YSLog::LoggingEvent *)

잘 살펴 보시면 mem_fun 에는 인자가 없는데, LogListener::publish 는 LoggingEvent * 를 인자로 갖기 때문에 에러가 발생한 걸 알 수 있습니다. 어~ 거참 이상하군요. 제가 기억하기로 mem_fun 함수 객체 어댑터는 인자가 있는 경우에도 쓸 수 있는데 말입니다. 그리고, 저는 Visual C++ 2005 Express 에서 컴파일을 이미 해 보았기 때문에 인자가 있는 경우에도 쓸 수 있는 것으로 분명히 기억하고 있습니다. 자 그럼 다음으로 확인해 봐야할 건 뭘까요 ? 그렇습니다. 매뉴얼을 찾아봐야 하겠지요. mem_fun을 찾아 봤더니 다음과 같이 나오는 군요.

mem_fun

template<class R, class T>
    mem_fun_t<R, T> mem_fun(R (T::*pm)());

The template function returns mem_fun_t<R, T>(pm).

이번에는 mem_fun_t 를 한 번 찾아보죠.

mem_fun_t

template<class R, class T>
    struct mem_fun_t : public unary_function<T *, R> {
    explicit mem_fun_t(R (T::*pm)());
    R operator()(T *p);
    };

The template class stores a copy of pm, which must be a pointer to a member function of class T, in a private member object. It defines its member function operator() as returning (p->*pm)().

음... 확실히 mem_fun 은 인자가 없는 멤버 함수에 대한 어댑터인 것 같습니다. mem_fun 이 어떻게 구현되어 있는지 확인해 봐야 겠습니다. <functional> 을 직접 확인해 봤더니 다음과 같이 mem_fun 이 정의되어 있네요.

  // TEMPLATE CLASS mem_fun_t
template<class _R, class _Ty>
 class mem_fun_t : public unary_function<_Ty *, _R> {
public:
 explicit mem_fun_t(_R (_Ty::*_Pm)())
  : _Ptr(_Pm) {}
 _R operator()(_Ty *_P) const
  {return ((_P->*_Ptr)()); }
private:
 _R (_Ty::*_Ptr)();
 };
  // TEMPLATE FUNCTION mem_fun
template<class _R, class _Ty> inline
 mem_fun_t<_R, _Ty> mem_fun(_R (_Ty::*_Pm)())
 {return (mem_fun_t<_R, _Ty>(_Pm)); }

위 정의로 보건대 확실히 mem_fun 은 인자를 받지 않는군요. 그 아래에 mem_fun1 을 확인해 봤더니 mem_fun1 이 하나의 인자를 받는 어댑터이네요.

  // TEMPLATE CLASS mem_fun1_t
template<class _R, class _Ty, class _A>
 class mem_fun1_t : public binary_function<_Ty *, _A, _R> {
public:
 explicit mem_fun1_t(_R (_Ty::*_Pm)(_A))
  : _Ptr(_Pm) {}
 _R operator()(_Ty *_P, _A _Arg) const
  {return ((_P->*_Ptr)(_Arg)); }
private:
 _R (_Ty::*_Ptr)(_A);
 };
  // TEMPLATE FUNCTION mem_fun1
template<class _R, class _Ty, class _A> inline
 mem_fun1_t<_R, _Ty, _A> mem_fun1(_R (_Ty::*_Pm)(_A))
 {return (mem_fun1_t<_R, _Ty, _A>(_Pm)); }

그렇다면 mem_fun 을 mem_fun1 으로 바꿔야겠군요. 그런데 이상하군요. 왜 Visual C++ 2005 Express 에서는 그냥 컴파일이 됐을까요 ? C++ 표준안 draft 버전에서는 mem_fun1 이라는 멤버 함수 어댑터가 있었는데, 실제 표준안이 최종 통과될 때는 mem_fun1 이 mem_fun 으로 바뀌었다는 군요. C++ 첫번째 표준안이 98년 제정되었고, Visual Studio 6가 98년에 출시되었으므로 Visual Studio 6 에 있는 STL 이 최종 표준안의 반영하지 못한 것으로 해석할 수 있을 것 같습니다.

이렇게 해서 문제의 원인은 파악했는데 이걸 어떻게 해결할까요 ? 우선 가장 쉽게 생각할 수 있는 해결책으로는 다음과 같은 방법을 생각할 수 있을 것 같네요.

#if defined(_MSC_VER) && (_MSC_VER <= 1200)
#define mem_fun mem_fun1
#endif


위 방법은 아주 간단히 해결할 수는 있지만, 나중에 mem_fun() 이 진짜로 필요하게 될 때는 문제가 발생할 소지가 있는 방법이라서 우리의 해결책에서 제외! 해야할 것 같군요.

또 다른 간단할 해결책으로 Visual C++ 의 predefined macro 인 _MSC_VER 값을 사용해서
아까 컴파일 에러 났던 부분을 다음과 같이 수정하면 어떨까요 ?

  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
#if defined(_MSC_VER) && (_MSC_VER <= 1200)
    bind2nd(mem_fun1(&LogListener::publish), pLogEv)
#else
    bind2nd(mem_fun(&LogListener::publish), pLogEv)
#endif
    );

맘에 드시나요 ? 전 왠지 구리게 느껴지네요. 매번 새로운 컴파일러에서 비슷한 문제점이 생길때마다 저런식으로 일일이 컴파일러의 predefined macro 를 체크해 봐야 한다는 게 별로 맘에 들지는 않네요. 더 우아한 해결책은 없을까요 ? 다음과 같은 방법은 어떨까요 ?

// Portability.h
#if defined(_MSC_VER) && (_MSC_VER <= 1200)
#define USE_MEM_FUN1
#endif

// LogSource.cpp
#include "Portability.h"
...
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
#ifdef USE_MEM_FUN1
    bind2nd(mem_fun1(&LogListener::publish), pLogEv)
#else
    bind2nd(mem_fun(&LogListener::publish), pLogEv)
#endif
    );


위와 같이 Portability 와 관련된 내용을 한 곳에 몰아 두면 새로운 컴파일러나 OS 를 지원해야할 때, 소스코드 이곳 저곳을 편집할 필요 없이 Portability 관련 내용이 정의되어 있는 파일에 새로운 컴파일러나 OS 의 특성을 표현하게 하면 Porting 작업이 훨씬 수월해 질 것입니다. 여기에서 한 술 더 떠서 다음과 같은 방법도 가능할 것입니다.

#ifndef PORTABILITY_H
#define PORTABILITY_H

#if defined(_MSC_VER) && (_MSC_VER <= 1200)

#include <functional>

namespace std {

template<class _R, class _Ty, class _A> inline
 mem_fun1_t<_R, _Ty, _A> mem_fun(_R (_Ty::*_Pm)(_A))
 {return (mem_fun1_t<_R, _Ty, _A>(_Pm)); };

}

#endif

#endif // PORTABILITY_H

// LogSource.cpp
#include "Portability.h"
...
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),

    bind2nd(mem_fun(&LogListener::publish), pLogEv));


즉, 그냥 인자를 하나 갖는 mem_fun() 을 직접 Portability.h에서 구현해 버리는 것이지요. 이렇게 하면 이식성에 관련된 대부분의 사항이 Portability.h 안에 정의되고, 다른 코드에서는 나타나지 않게 되므로 코드가 훨씬 간결해지고, 고쳐야 하는 부분도 상대적으로 더 적어지게 될 것이므로 더 바람직한 방법이라고 판단됩니다.

다음은 Linux 를 위한 Build 파일(Makefile)!!

# Makefile
all: YSLog

OBJ_DIR = ./obj
SRC_DIR = ./src
INC_DIR = ./h

OBJECTS = $(OBJ_DIR)/Main.o \
 $(OBJ_DIR)/LogSource.o \
 $(OBJ_DIR)/SimpleLogSink.o

INCLUDES = -I$(INC_DIR)
OPTIONS = -Wall -g -c -O
CFLAGS = $(OPTIONS) $(INCLUDES)

YSLog: $(OBJECTS)
 g++ -o YSLog $(OBJECTS)

$(OBJ_DIR)/%.o:$(SRC_DIR)/%.cpp
 g++ $(CFLAGS) -o $@ $<

$(OBJ_DIR)/Main.o: $(INC_DIR)/LogSource.h $(INC_DIR)/LogPublisher.h \
  $(INC_DIR)/LogListener.h $(INC_DIR)/LoggingEvent.h \
  $(INC_DIR)/Portability.h

$(OBJ_DIR)/LogSource.o: $(INC_DIR)/LogSource.h $(INC_DIR)/LogPublisher.h \
  $(INC_DIR)/LogListener.h $(INC_DIR)/LoggingEvent.h \
  $(INC_DIR)/Portability.h $(INC_DIR)/SimpleLogSink.h

$(OBJ_DIR)/SimpleLogSink.o: $(INC_DIR)/SimpleLogSink.h \
  $(INC_DIR)/LogListener.h $(INC_DIR)/LoggingEvent.h

clean:
 rm -rf $(OBJ_DIR)/*.o YSLog



(Visual C++ 6 와 Linux에서 컴파일 가능한 소스 및 빌드 환경 첨부 합니다)


위 빌드 파일에 문제가 있을 경우 댓글로 알려 주시면 감사하겠습니다. 어떤 분은 g++ 3.2.2 에서 -O 옵션을 주고 컴파일하면 네임스페이스를 쓴 코드에서 에러가 난다고 하더군요. Linux에서 제가 쓴 컴파일러는 g++ 3.4.4 였는데, 문제 없이 컴파일되더군요.

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.

신고
  1. 낭인

    2007.09.04 02:48 신고 [수정/삭제] [답글]

    재밌네요..
    저는 주로 임베디드 환경을 사용하는데 여기에서는 표준stream 쓰기가 쉽지 않아
    출력은 uart로 하고,
    Trace 도 수많은 모듈별로 module 별로 하기 때문에 여기서 사용하는 5개정도로 간단하게 사용하지 않고 255개는 충분히 넘게 사용하고 있답니다.

    • 김윤수

      2007.09.04 03:23 신고 [수정/삭제]

      어떤 이유 때문에 표준 스트림을 쓰기 어려우신 건가요 ? 이유를 좀 알 수 있을까요 ? C++ 표준 라이브러리가 아예 표함되어 있질 않나요 ? 아니면 표준 iostream 라이브러리만 빠져 있는 건가요 ?

    • 김윤수

      2007.09.04 03:25 신고 [수정/삭제]

      제가 설계하고 있는 로깅 라이브러리도 module 별로 로깅할 수 있도록 하는 기능을 개발 중이므로 모듈 수는 문제가 없을 것 같습니다. 로깅 라이브러리 첫글부터 보시면 모듈별 로깅이 가능하도록 하는 것을 보실 수 있습니다. LogSource 자체가 모듈 하나에 대응된다고 보시면 됩니다.

      제 질문에 답변을 주시면 좋겠는데, 언제 또 들리실지 모르겠네요. 아무튼 댓글 감사합니다.

  2. 낭인

    2007.09.04 02:58 신고 [수정/삭제] [답글]

    그리고 출력에 걸리는 delay때문에 중간에 queue를 하나 두고 publisher가 queue 출력하고 별도의 thread에서 queue에서 개별 listener로 할당해주는 것도 고려해보시는게 어떨까 싶습니다.

    • 김윤수

      2007.09.04 03:21 신고 [수정/삭제]

      예, 말씀하신 방법을 생각한 적이 있긴 합니다. 지금 수준에서 거기까지 논할 단계가 아니긴 하지만 한참 후에 완성도가 높아질 때 쯤에 thread-safe, multi-threaded 특성을 고려하여 코드를 수정해 볼 생각입니다. embedded 환경이라면 주로 어떤 환경을 쓰시나요 ? VxWorks, ThreadX 이런 거 쓰시나요 ?

      저도 embedded 환경을 쓰시는 분들의 의견을 좀 듣고 싶은데... 정보가 없어서 어렵네요.

  3. 낭인

    2007.09.04 20:46 신고 [수정/삭제] [답글]

    1. Linux제외 Phone 개발환경에 C/C++ 표준라이브리 자체가 포함되어 있지 않습니다. 정말 C/C++ 만 씁니다. malloc/new 대신 개발환경에 modified 된 MALLOC/NEW를 사용합니다. 그래서 표준스트림 사용하는게 어렵습니다. 적합한 예가 될지 모르지만 open()한다고 해서 파일이 열리지 않습니다. modified_open()를 파일 오픈을 할 수 있습니다. 차라리 메모리에 보관했다가 필요에 따라 Uart/Network/File 로 전환 할 수 있는게 범용적이지 않을까 생각됩니다.
    2.예를 들어 sound(warn,err,..) usb(warn,err,..) flash(warn,err,..) 별로 크게 모듈을 나눈다면 필터링에서 sound 모듈에 관련된 trace만 필터링해서 출력할 수 있는지 궁금했습니다. 제 이해가 낮았나 봅니다.
    3. nuclues 썼는데 지금은 high level OS로 전환하려고 휴식중입니다.

    • 김윤수

      2007.09.04 21:50 신고 [수정/삭제]

      그렇군요. 또 질문이 몇 가지 생기는데...
      표준 라이브러리가 포함되어 있지 않다면 standard template library 도 전혀 포함되어 있지 않나요 ? 그리고, 컴파일러가 아예 template 도 지원하지 않는지요 ?
      iostream 을 못 쓴다면 제가 구현 중인 로깅 라이브러리에서는 LogListener 를 상속 받아서 Uart/Network/File 로 로깅하는 클래스를 따로 구현해야 할 것 같습니다. 현재 SimpleLogSink 는 그냥 가장 간단하게 iostream 으로 출력하는 기능만 있으니까요.

      다시 한 번 답변 감사드립니다.

  4. goshujin

    2007.09.11 19:45 신고 [수정/삭제] [답글]

    1) main.cpp 의 42 라인 YSLOG_DEBUG(root, "Error occurred, error code = 100") 에서 오류가 발생합니다. (저만 그런 것인지는 잘 모르겠습니다.)

    2) 정상적으로 파일을 읽고 쓸수 없는 경우의 예외 처리 기능은 없는건가요?

    3) 그리고, 소스에서 Project Settings / Debug / Working directory 에서 C:\DOCUMENTS AND SETTINGS\김윤수\MY DOCUMENTS\내 작업\YSLog 는 삭제하여 주세요.

    • 김윤수

      2007.09.12 00:01 신고 [수정/삭제]

      답변입니다.
      1) 이상하네요. 다 컴파일해 봤는데 에러가 나지 않았는데... 에러 메시지를 한 번 메일로 보내주실 수 있을까요 ? 컴파일러랑 OS 환경도 알려주시면 도움이 될 것 같습니다. 여기에 댓글 남겨 주셔도 되고 아니면 제 e-mail 로 알려주셔도 됩니다. 제 e-mail 을 오른쪽 위 제 사진 밑에 있습니다.
      2) 예외 처리 기능 당연히 넣어야지요. ^^; 요즘 제가 회사일 땜에 정신이 없어서 전혀 update 를 못하고 있습니다.
      3) 다음에 혹시 소스를 다시 릴리즈 하게 되면 삭제하도록 하겠습니다.
      즐 블로깅하세요~

  5. goshujin

    2007.09.12 18:07 신고 [수정/삭제] [답글]

    수고하십니다. 자꾸 괴롭혀 드리네요ㅎㅎ. 9월까지 업무가 약간 널널해져서 오픈 소스 보면서
    연구 중입니다.^^;
    아래 링크에 제가 실행 시에 문제점을 올립니다.
    더 필요한 정보가 있으면 말씀하세요.
    http://j2doll.tistory.com/attachment/dl140.zip

  6. Kaizen

    2011.01.09 22:22 신고 [수정/삭제] [답글]

    안녕하세요.
    글 잘 읽고 있습니다.

    작성하신 로깅 라이브러리는 현재 어느정도 진행되었나요? 공유해 주실 수 있나요?.
    진행하는 컨샙이 일반성이 있어서 완성을 하셨다면 저도 사용하고 싶습니다.

    이전 글에서 올려주신 소스는 확인해 보았으나 아직 시작단계인 것 같습니다....

댓글을 남겨주세요.

티스토리 툴바