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

Posted at 2007. 8. 21. 08:16 // in S/W개발/고절가주팁 // by 김윤수


고절가주팁 열한번째 시간입니다. (로깅 라이브러리 관련된 글을 처음 보시는 분들은 본문에 나와 있는 여기를 클릭해 보시면 이전 글들을 모두 보실 수 있습니다). 지난 시간에는 LogSource 까지 구현한 후 Code Review 하다가 멈췄었지요. 지난 시간에 확인은 했지만 해결은 나중으로 미뤘던 문제가 두 가지 있었습니다. 뭐였는지 기억하시나요 ?

1. LogListener.h 와 LoggingEvent.h 구현
2. log(lvl, file, func, line, msg) 안에서 isEnabled() 가 호출된 후 다시 log(pLogEv) 에서도 중복해서 호출되는 문제

이 두 가지 중 먼저 2번 문제에 대한 저의 해결책을 제시해 보겠습니다. 여러분이 생각한 것과 한 번 비교해 보시기 바랍니다.

저라면 log(lvl, file, func, line, msg) 와 log(pLogEv) 를 다음과 같이 Refactoring 하겠습니다.

bool LogSource::log(
  LogLevel lvl,
  const char* file,
  const char* func,
  int line,
  const char* msg) const
{
  if (!isEnabled(lvl))
    return false;

  LoggingEvent* pLogEv = new LoggingEvent(
    _module.c_str(), lvl, file, func, line, msg);

  return unconditionalLog(pLogEv);
}

bool LogSource::log(LoggingEvent* pLogEv) const
{
  if (!isEnabled(pLogEv->level()))
    return false;

  return unconditionalLog(pLogEv);
}

그리고 나서 unconditionalLog(pLogEv) 는 다음과 같이 작성하면 될 것입니다.

bool LogSource::unconditionalLog(LoggingEvent* pLogEv) const
{
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
   &LogListener::publish(pLogEv));
}

즉, 두 log() 멤버 함수 모두 먼저 isEnabled()를 체크한 후에 unconditionalLog()-로깅 가능 여부를 체크하지 않고 무조건 로깅하는 멤버 함수-를 호출하도록 하는 것이지요. unconditionalLog(pLogEv) 말고 unconditionalLog(lvl, file, func, line, msg) 도 필요하려나요 ? 한 번 두고 볼 일입니다.

log() 와 unconditionalLog() 의 구현은 이렇게 바꾼다고 치고... unconditionalLog() 의 접근 권한은 어떻게 줘야할까요 ? public 으로 할까요 아니면 private 으로 할까요 ? 즉, 외부에서 사용하는 인터페이스로 봐야 할까요 ? 아니면 구현으로 봐야 할까요 ? 지금으로 봐서는 unconditionalLog()는 내부 구현에 가까운 게 아닌가라는 생각이 드네요. 그래서 다음과 같이 private 으로 선언하도록 하겠습니다(지금은 그렇다 치는데 나중에 어떻게 바뀔지 두고 볼 일입니다).

class LogSource : public LogPublisher {
private:
  string _module;
  ......
  bool unconditionalLog(LoggingEvent* pLogEv) const;
  ......
};

2번 문제는 이 정도로 일단락시키고, 나머지 Code Review 를 더 진행해 보도록 하겠습니다.
자~! LogSource.cpp 코드를 다시 한 번 자세히 들여다 볼까요 ?

#include <algorithm>
#include <functional>
#include "LogSource.h"

using namespace std;

namespace YSLog {

bool LogSource::subscribe(LogListener* pListener)
{
  _lListenerPtr.push_back(pListener);

  return true;
}

bool LogSource::unsubscribe(LogListener* pListener)
{
  _lListenerPtr.remove(pListener);

  return true;
}

bool LogSource::isEnabled(LogLevel lvl) const
{
  return (_enable && lvl >= _level && !_lListenerPtr.empty());
}

string LogSource::module() const
{
  return _module;
}

bool LogSource::log(
  LogLevel lvl,
  const char* file,
  const char* func,
  int line,
  const char* msg) const
{
  if (!isEnabled(lvl))
    return false;

  LoggingEvent* pLogEv = new LoggingEvent(
    _module.c_str(), lvl, file, func, line, msg);

  return unconditionalLog(pLogEv);
}

bool LogSource::log(LoggingEvent* pLogEv) const
{
  if (!isEnabled(pLogEv->level()))
    return false;

  return unconditionalLog(pLogEv);
}

bool LogSource::unconditionalLog(LoggingEvent* pLogEv) const
{
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
   &LogListener::publish(pLogEv));

  return true;
}

};


제 눈에 띄는 부분은 표시를 해 두었는데요. 여러분은 어떤 부분이 눈에 뜨이셨나요 ? 댓글을 통해서 잘못된 부분이나 개선할 부분이 있으면 얘기해 주세요~ 제발요~

unconditionalLog() 부터 살펴 보겠습니다. STLfor_each() 가 멤버 함수 포인터를 넣어 주면 알아서 멤버 함수를 호출해 줬었는지 기억이 가물 가물 하네요. for_each() 가 어떻게 선언되어 있는지 먼저 살펴 봐야 겠습니다.

template <class InputIterator, class UnaryFunction>
UnaryFunction for_each(InputIterator first, InputIterator last, UnaryFunction f);

Requirements on types

  • InputIterator is a model of Input Iterator
  • UnaryFunction is a model of Unary Function
  • UnaryFunction does not apply any non-constant operation through its argument.
  • InputIterator's value type is convertible to UnaryFunction's argument type.
위에 제시된 Requirements 중에 UnaryFunction 이 눈에 뜨이네요. 이 Unary Function 은 다시 다음과 같이 정의되어 있습니다.

A Unary Function is a kind of function object: an object that is called as if it were an ordinary C++ function. A Unary Function is called with a single argument.
이 Unary Function 이란 녀석은 보통의 C++ 함수와 똑같이 호출될 수 있어야 하고, 인자가 하나라고 하네요. 저희가 정의한 &LogListener::publish(pLogEv) 는 인자가 하나이지만 보통의 C++ 함수가 아니라 멤버 함수라는 점이 틀리네요. 그리고 잘 생각해 보면 for_each() 는 주어진 Unary Function 의 인자로 InputIterator 의 값을 넣어주지 pLogEv 를 인자로 넣어주진 않습니다. 따라서 인자도 잘못된 것 같습니다. 다음의 for_each() 정의를 살펴 보시면 그 의미가 명확해 집니다.

template <class InputIterator, class UnaryFunction>
UnaryFunction
for_each(InputIterator first, InputIterator last, UnaryFunction f)
{
  for (; first!= last; ++first)
    f(*first);
  return (f);
}

이렇게 되어 있는 이상 unconditionalLog() 가 컴파일될리가 없을 것입니다. 어찌됐든 저희들은 _lListenerPtr의 각 멤버에 대해 LogListener::publish(pLogEv) 가 호출되게 하고 싶은데 어떻게 해야할까요 ? 혹시 제가 이전에 시리즈물로 작성한 C++ 이야기를 읽으신 적이 있으신지 모르겠습니다. 그 중에 C++ 이야기 열다섯번째: 함수 객체 #3, 멤버 함수 어댑터를 보시면 멤버 함수를 함수 객체로 만들어 주는 녀석이 있습니다. 그걸 쓰면 해결할 수 있을 것 같습니다.

bool LogSource::unconditionalLog(LoggingEvent* pLogEv) const
{
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
    mem_fun(&LogListener::publish(pLogEv)));

  return true;
}


이렇게 하면 될까요 ?  멤버 함수 어댑터는 멤버 함수 자체를 인자로 받아야 합니다. 특정 인자와 binding 된 멤버 함수를 인자로 받을 수가 없습니다. 즉, 다음과 같이 해야 한다는 것이지요.

mem_fun(&LogListener::publish)

따라서 pLogEv 를 인자로 주고 싶다면 별도로 binding 하는 방법을 사용해야 합니다. 이걸 위해서는 C++ 이야기 열두번째: 함수 객체 #1에 제시된 bind2nd()를 사용하시면 됩니다. 왜 bind1st() 가 아니라 bind2nd() 이냐라는 질문이 떠오를 수 있는데요. 그건 이렇습니다.

mem_fun() 멤버 함수 어댑터는 인자가 하나 있는 멤버 함수를 만나면 첫번째 인자는 객체 인스턴스이고 두번째 인자가 멤버 함수의 본래 인자인 함수 객체를 만들어 냅니다. 즉, 마치 다음과 같은 함수 객체가 만들어지는 것이지요.

struct XXX {
operator () (LogListener* pListener, LoggingEvent* pLogEv);
};


따라서 bind2nd() 를 이용해서 두번째 인자를 pLogEv 로 bind 해주어야 하는 것입니다. 이걸 코드로 표현하면 다음과 같습니다.

bool LogSource::unconditionalLog(LoggingEvent* pLogEv) const
{
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
    bind2nd(mem_fun(&LogListener::publish), pLogEv));

  return true;
}


Code Review 의 마지막으로 isEnabled() 와 관련된 문제를 짚어 보겠습니다. 제가 추측하기에 isEnabled() 는 로깅 라이브러리의 인터페이스 중 가장 자주 호출되는 멤버 함수일 것으로 예상됩니다. 왜냐하면 고절가주팁 #8에서 설계한 매크로 함수 인터페이스에서 isEnabled() 가 호출된 이후에 log() 멤버 함수가 호출되도록 되어 있기 때문입니다. 그렇다면 isEnabled() 의 수행 속도가 최적화되도록 하는 것이 성능상 이로울 것입니다.

isEnabled() 의 수행 성능을 개선하려면 어떻게 해야할까요 ? 알고리즘을 봐서는 더 이상 최적화하고 말 것도 없어 보이는데 말입니다. 그렇다면 isEnabled() 를 inline 으로 선언하면 어떨까요 ? function call overhead 가 사라지므로 상당한 성능상의 효과를 볼 수 있을 것 같습니다.

다음과 같이 isEnabled() 구현을 LogSource.h 로 옮기도록 하겠습니다.

class LogSource : public LogPublisher {
public:
  LogSource(string mod, bool enable = true, LogLevel lvl = YSLog::WARN) :
    _module(mod), _enable(enable), _level(lvl), _lListenerPtr()
  {
  }
  ......
  bool isEnabled(LogLevel lvl) const
 
{
    return (_enable && lvl >= _level && !_lListenerPtr.empty());
  }
  ......
};

이제 다시 구현으로 넘어 와서 매크로 함수를 구현해 보도록 하겠습니다. 고절가주팁 #8에서 YSLOG_LOG() 라는 매크로 함수는 이미 구현했었습니다.

#define YSLOG_LOG(logSource, lvl, msg) \
  { \
    if ((logSource).isEnabled((lvl))) { \
      (logSource).log((lvl), __FUNCTION__, __FILE__, __LINE__, \
        (msg)); \
    } \
  }


이 외에 로깅 수준별로 별도의 매크로 함수 YSLOG_DEBUG(), YSLOG_TRACE(), YSLOG_INFO(), YSLOG_WARN(), YSLOG_ERROR(), YSLOG_FATAL() 등을 정의했었습니다. 이 매크로 함수들은 YSLOG_LOG() 를 이용해서 다음과 같이 정의할 수 있을 것입니다.

#define YSLOG_DEBUG(logSource, msg) \
  YSLOG_LOG((logSource), YSLog::DEBUG, (msg))
#define YSLOG_TRACE(logSource, msg) \
  YSLOG_LOG((logSource), YSLog::TRACE, (msg))
#define YSLOG_INFO(logSource, msg) \
  YSLOG_LOG((logSource), YSLog::INFO, (msg))
#define YSLOG_WARN(logSource, msg) \
  YSLOG_LOG((logSource), YSLog::WARN, (msg))
#define YSLOG_ERROR(logSource, msg) \
  YSLOG_LOG((logSource), YSLog::ERROR, (msg))
#define YSLOG_FATAL(logSource, msg) \
  YSLOG_LOG((logSource), YSLog::FATAL, (msg))

이렇게 하고 났더니 한 가지 또 고민되는 문제가 생겼네요. YSLOG_LOG() 에서 isEnabled() 를 호출한 후, log() 를 호출하는데, log() 안에서 다시 isEnabled() 가 호출된다는 문제가 있네요. 이 문제를 해결하려면 log() 를 호출하는 게 아니라 unconditionalLog() 를 호출해야할 것 같은데... unconditionalLog() 의 access permission 이 private 되어 있어서 쓸 수가 없습니다. 그냥 public 으로 바꾸기에는 약간 꺼림직한 것이 unconditionalLog() 를 프로그래머들이 마음대로 사용한다면 LogSource 의 필터링 메커니즘이 제대로 작동하지 않게 된다는 것 때문입니다. 즉, 해당 모듈의 로깅 활성화 여부와 로깅 수준에 상관 없이 무조건 로깅되어 버린다는 문제가 있는 것입니다.

여러분이라면 이 문제를 어떻게 해결하시겠어요 ? 이렇게 1)여러 측면의 품질 특성이 충돌을 일으킬 때는 여러 품질 특성을 모두 만족시켜주는 해결 방법을 찾던지 아니면 2)Design Goal 에 비추어 우선 순위에 따라 설계를 조정하는 방법이 있습니다. 이 예에서는 성능과 잘못된 사용 예방 이 두 특성간의 충돌이라고 할 수 있습니다. 여러분이라면 어떤 선택을 할 것 같으세요 ?

저는 고절가주팁 #7 에서 정리했던 Design Goal 다시 한 번 상기해 봤습니다.

1. 로깅 라이브러리가 최대한 다양한 환경에서 사용될 수 있도록 한다.
2. 사용하기 쉽게 만든다.
3. 로깅 라이브러리의 크기를 최대한 작게 만든다.


성능은 1번 Design Goal 과 관련이 있고, 잘못된 사용 예방은 2번 Design Goal 과 관련이 있습니다. 제가 이상의 Design Goal을 설정할 때 우선 순위에 따라 나열한 것이었습니다. 따라서 저는 1번을 먼저 고려하여 unconditionalLog() 를 public 으로 바꾸고 YSLOG_LOG() 구현을 바꾸도록 하겠습니다.

class LogSource : public LogPublisher {
public:
  ......
  bool unconditionalLog(LoggingEvent* pLogEv) const;
};

#define YSLOG_LOG(logSource, lvl, msg) \
  { \
    if ((logSource).isEnabled((lvl))) { \
      YSLog::LoggingEvent* pLogEv = new YSLog::LoggingEvent( \
        (logSource).module().c_str(), (lvl), \
        __FUNCTION__, __FILE__, __LINE__, (msg)); \
      (logSource).unconditionalLog(pLogEv); \
    } \
  }


어떻습니까 ? 이 정도면 저는 상당히 괜찮은 코드를 작성했다고 생각하는데 여러분은 어떠신가요 ? 아직 더 고칠 부분이 보이신다면 댓글 달아주시면 감사하겠습니다. 여러분과의 논의 가운데 로깅 라이브러리가 더 발전하게 될 것입니다.

다음 글에서는 LoggingEvent 와 SimpleLogSink 를 상세하게 설계해 보고 구현해 보도록 하겠습니다.

그럼 지금까지 제 글을 읽어 주셔서 감사드리고, 이 글이 맘에 드신다면 살짝 여기 저기 추천 부탁드립니다. 고절가주팁이 쭉~ 갈 수 있도록 많은 관심 부탁드립니다.

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