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

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


고절가주팁 열번째 시간입니다. 제목 형식을 또 바꾸었습니다. 고절가주팁이라고만 제목을 썼더니 이전 글에 비해 방문자가 확 줄더군요. 아직은 고절가주팁이라고 줄일 때가 아닌 것 같습니다. 아무래도 제목을 보고 이끌려 오는 분이 아직은 고정 독자보다는 훨씬 많은 듯 합니다.

그나저나 고절가주팁글이 미디어몹 헤드라인에 한 반나절 걸렸습니다. 안타깝게도 화면 캡쳐하기 전에 내려와서 캡쳐한 이미지는 보여드릴 수 없지만 다음 이미지를 보시면 헤드라인에 걸렸었다는 걸 짐작하실 수 있을 것 같네요.

미디어몹 헤드라인에 걸린 고절가주팁 #8

미디어몹 헤드라인에 걸린 고절가주팁 #8


미디어몹 방문 댓글

미디어몹 방문 댓글


고절가주팁 같은 글이 미디어몹 헤드라인에 올라간다는 게 참 신기하더군요. 상당히 좁은 독자층을 위한 글인데 말입니다. 아무튼 기분은 참 좋았습니다.

이번에는 저번 글에 이어서 LogSource 를 멤버 함수까지 완전히 구현하고 Code Review 까지 해 보도록 하겠습니다(이번에는 다른 글보다 약간 더 깁니다. 여유있는 시간에 찬찬히 보시면 좋을 것 같네요). 지난 번까지 설계한 LogSource 의 완전한 인터페이스 및 멤버 변수는 다음과 같습니다.

#ifndef LOGSOURCE_H
#define LOGSOURCE_H

#include <list>
#include <string>
#include "LogPublisher.h"
#include "LogListener.h"           // TODO: 작성해야 함
#include "LoggingEvent.h"
          // TODO: 작성해야 함

using namespace std;

namespace YSLog {

class LogSource : public LogPublisher {
private:
  string _module;
  bool _enable;
  LogLevel _level;
  list<LogListener*> _lListenerPtr;
 
public:
  LogSource(string mod, bool enable = true,
            LogLevel lvl = YSLog::WARN) :
    _module(mod), _enable(enable), _level(lvl), _lListenerPtr()
  {
  }

  virtual bool subscribe(LogListener* pListener);
  virtual bool unsubscribe(LogListener* pListener);

  bool isEnabled(const LogLevel lvl) const;
  string module() const;
  bool log(const char* file,
           const char* func,
           int line,
           const char* msg) const;
  bool log(LoggingEvent* pLogEv) const;
};

}

#endif // LOGSOURCE_H

저번에 작성할 때까지는 몰랐는데, 다시 한 번 확인해 봤더니 인터페이스를 한 가지 잘못 설계했더군요. 바로 log(file, func, line, msg) 멤버 함수를 잘못 설계했습니다. 뭘 잘못 설계했냐구요 ? 글쎄요 뭘까요 ? 백마디 말보다 한 줄 코드가 낫다고 수정된 인터페이스를 보여 드리겠습니다.

  bool log(LogLevel lvl,
           const char* file,
           const char* func,
           int line,
           const char* msg) const;


어디가 잘못 됐는지 아시겠죠 ? 현재 로깅하는 로깅 정보의 수준이 어떤지를 알려주지 않았던 게 잘못됐었습니다. 그렇다면 모델링했던 UML 다이어그램도 수정해야겠지요 ?

수정된 LogSource 설계

수정된 LogSource 설계

잘못된 설계 Defect을 하나 수정했네요. 이제 멤버 함수들을 구현해 보도록 하지요.

먼저 subscribe() 와 unsubscribe()! 간단합니다. subscribe() 가 호출되면 list 컨테이너push_back() 멤버 함수를 써서 list 에 추가하고, unsubscribe() 가 호출되면 remove() 멤버 함수를 써서 list 에서 삭제하면 됩니다. STLlist 컨테이너가 이 모든 복잡한 것들을 구현해 놓고 있기 때문에 우리는 그냥 이용만 하면 됩니다.

// LogSource.cpp
#include "LogSource.h"           // 이미 <list>를 include 함

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

  return true;                   // 그냥 무조건 true 를 리턴 ??
}

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

  return true;                  // 그냥 무조건 true 를 리턴 ??
}

그 다음은 isEnabled(lvl) ! 이것도 간단합니다!! 현재 모듈에 대해 로깅이 활성화되어 있고(AND 조건이죠), 질의 대상이 되는 로깅 수준(lvl 인자)이 내부적으로 기억하고 있는 활성화된 로깅 수준(_level 멤버 변수)과 같거나 보다 더 크다면 true 를 리턴하면 될 것입니다.

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


module() 멤버함수는 당연히 다음과 같이 하면 될 것이구요. 두 말하면 잔소리죠.

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


log(lvl, file, func, line, msg) 멤버함수! 이것도 간단하~~~~~~~~~~~~~~~~~~~~ㄹ까요 ? 음... 그리 간단할 것 같지 않네요. 왜 그럴까요 ? 고절가주팁 #7 에서 언급한 LogListener 의 인터페이스가 다음과 같았다는 것을 기억하시나요 ?

사용자 삽입 이미지

로깅 라이브러리 기본 클래스

LogSource 의 log() 멤버 함수에서는 lvl, file, func, line, msg 와 같은 인자를 받아서 로깅하는 데, LogListener 에서는 LoggingEvent 를 인자로 받으니 5개의 인자를 LoggingEvent 로 바꿔주는 작업이 필요할 것입니다. 그리고, log() 류의 멤버함수에는 log(LoggingEvent* pLogEv) 도 있었습니다. 그렇다면 log(lvl, file, func, line, msg) 멤버함수는 5개의 인자를 LoggingEvent 로 바꾼 후에 log(pLogEv)를 호출하면 될 것 같네요.

bool LogSource::log(
  LogLevel lvl,
  const char* file,
  const char* func,
  int line,
  const char* msg) const
{
  // 로깅할 필요가 있는지 필터링합니다. 여기서 필터링하지 않으면 쓸 데 없이
  // LoggingEvent 객체 인스턴스를 생성하는 작업을 하게 될 것입니다.
  if (!isEnabled(lvl))
    return false;

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

  return log(pLogEv);
}


이제 마지막으로 log(pLogEv) 를 코딩할 차례이네요. log(pLogEv) 가 해야하는 가장  중요한 일은 현재 subscribe 되어 있는 모든 LogListener 들에게 pLogEv 를 뿌려주는 일이겠지요. 그렇다면 다음과 같이 코드를 작성하면 되지 않을까요 ?

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

  for(list<LogListener*>::iterator i = lListenerPtr.begin();
      i != _lListenerPtr.end();
      i++) {
    (*i)->publish(pLogEv));
    // pLogEv 에 대한 소유권을 이미 넘겨 주었기 때문에 log() 는 pLogEv를
    // 위해 할당된 메모리를 릴리즈하지 않습니다. 그렇다면 subscribe 한
    // LogListener 가 여러개 있을 때는 어떤 일이 벌어질까요 ?
  }

  return true;
}


그런데, 그냥 이렇게 작성하려고 했더니 왠지 코드가 구질구질해 보이네요. for 문장에서 실질적인 실행 코드는 (*i)->publish(pLogEv) 인데, loop 제어 코드만 복잡하게 많으니까 상당히 밸런스가 맞지 않는 느낌이 납니다. 어떻게 이 코드를 상당히 엘레강스하면서도 뷰티풀하면서도 우아하게 만들 수 없을까요 ? ......

한참 생각을 해 봤더니 STLfor_each() 알고리즘이 생각나네요(이런 게 생각이 나려면 STL을 공부해서 미리 알고 있어야 하겠지요 ? 어떻게 해서 for_each()가 생각 났는지는 이 말로 대신 설명할 수 있을 것 같네요). for_each() 를 쓴다면 다음과 같이 고칠 수 있을 것 같다는 생각이 듭니다.

#include <algorithm>       // for_each 정의

using namespace std;

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

  for_each(_lListenerPtr.begin(), _lListenerPtr.end(), 
    &LogListener::publish(pLogEv));

  return true;
}


여기까지 대강 코드를 다 작성한 것 같으니 한 번 우리 Self Code Review 란 걸 한 번 해 보겠습니다. Self Code Review 란 자신이 작성한 코드에 문제가 없는지 자세히 살펴 보면서 눈으로 디버깅하는 것이다라는 정도로 알고 계시면 될 듯합니다.

우선 LogPublisher.h 부터 한 번 자세히 들여다 보겠습니다.

// LogPublisher.h
#ifndef LOGPUBLISHER_H          // include guard,
고절가주팁 #1
#define LOGPUBLISHER_H

namespace YSLog {               // 로깅 라이브러리의 namespace

struct LogListener;             // forward declaration

// struct idiom
struct LogPublisher {
  virtual bool subscribe(LogListener* pListener) = 0;
  virtual bool unsubscribe(LogListener* pListener) = 0;
};

}

#endif  // LOGPUBLISHER_H

뭐 잘못된 게 보이시나요 ? 크게 잘못된 것 없어 보이지만 LogListener 에 대해 forward declaration 한 게 걸리네요. 그냥 LogListener.h 를 include 하면 안될까요 ? 아! 우리가 현재까지는 아직 LogListener.h 를 구현하지 않았네요. 그러니 일단은 LogListener.h 는 비워둔채로 forward declaration 하는 게 맞을 것 같습니다. 이번에는 그냥 넘어가겠습니다.

LogPublisher 인터페이스를 struct 로 정의한 것도 struct 에서는 모든 멤버가 기본으로는 public 이 되는 것을 이용한 것이므로 별 문제 없습니다.

그 다음은 LogSource.h 를 살펴 보겠습니다.

#ifndef LOGSOURCE_H
#define LOGSOURCE_H

#include <list>
#include <string>
#include "LogPublisher.h"
#include "LogListener.h"           // TODO: 작성해야 함
#include "LoggingEvent.h"          // TODO: 작성해야 함

using namespace std;

namespace YSLog {

class LogSource : public LogPublisher {
private:
  string _module;
  bool _enable;
  LogLevel _level;
  list<LogListener*> _lListenerPtr;
 
public:
  LogSource(string mod, bool enable = true,
            LogLevel lvl = YSLog::WARN) :
    _module(mod), _enable(enable), _level(lvl), _lListenerPtr()
  {
  }

  virtual bool subscribe(LogListener* pListener);
  virtual bool unsubscribe(LogListener* pListener);

  bool isEnabled(const LogLevel lvl) const;
  string module() const;
  bool log(LogLevel lvl,
           const char* file,
           const char* func,
           int line,
           const char* msg) const;
  bool log(LoggingEvent* pLogEv) const;
};

}

#endif // LOGSOURCE_H

LogSource.h 를 살펴 봤더니 결국 LogListener.h 와 LoggingEvent.h 를 구현할 필요성이 생기네요. 게다가 LogLevel 도 정의되어 있질 않네요. 이것들을 다 forward declaration 으로 할까요 ? 일단 LogLevel 은 forward declaration 이 불가능할 것 같습니다.YSLog::WARN 이라는 건 이미 default 값으로 쓰고 있기 때문입니다. 그렇다면 LogListener 와 LoggingEvent 를 여기서 forward declaration 으로 하는 건 충분한가요 ? 잘 생각해 보면 LogSource.h 를 정의하는 데는 충분해 보입니다. 그렇지만 LogSource.cpp 에서는 멤버 함수들을 구현할 때 결국 LogListener 의 정확한 인터페이스와 LoggingEvent 의 정확한 인터페이스가 필요할 것입니다. 그러니 차라리 LogListener.h 와 LoggingEvent.h 를 지금 작성해 두는 게 좋겠습니다. 두 가지 큰 코드 defect 을 찾아냈네요. LogListener.h 와 LoggingEvent.h 는 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);
}

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 log(pLogEv);
}

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

  // pLogEv 에 대한 소유권을 이미 넘겨 주었기 때문에 log() 는 pLogEv를
  // 위해 할당된 메모리를 릴리즈하지 않습니다. 그렇다면 subscribe 한 
  // LogListener 가 여러개 있을 때는 어떤 일이 벌어질까요 ?
  for_each(_lListenerPtr.begin(), _lListenerPtr.end(),
    &LogListener::publish(pLogEv));

  return true;
}

};

제가 약간 문제가 있을 것 같은 부분을 미리 체크해 놓았습니다. 여러분은 제가 표시한 부분에 어떤 문제가 있을 것 같으세요 ?

isEnable() 멤버 함수 구현부터 살펴 볼까요 ? 여기에서 한 가지 놓친 부분이 있는데요... 잘 생각해 보면 subscribe 한 LogListener 가 없는 경우에도 로깅 정보는 결국 로깅되지 않을 것입니다. 그러니 isEnabled() 수준에서 false 를 리턴한다면 쓸데 없이 log(lvl, file, func, line, msg) 에서 LoggingEvent 객체 클래스를 생성하는 코드랑 log(pLogEv) 의 for_each() 알고리즘을 호출하는 코드를 실행하지 않게 될 것입니다. 이런 불필요한 코드 호출을 막기 위해 isEnable() 멤버 함수 구현을 다음과 같이 바꿔야 겠습니다.

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

그 다음에 log(lvl, file, func, line, msg) 와 log(pLogEv) 를 찬찬히 한 번 보시겠어요 ? 어째 쪼~금 꺼림직한 게 보이지 않으세요 ? log(lvl, file, func, line, msg) 안에서 이미 isEnabled() 를 호출해서 필터링을 했는데 log(pLogEv) 안에서도 또 isEnabled() 를 호출하면서 필터링을 하고 있습니다. 쓸데 없이 중복되는 코드가 있습니다. 이걸 어떻게 해결해야 하나요 ? 고민이군요. 여러분이라면 어떻게 해결하시겠어요 ? 좋은 아이디어 알려주시면 정말 감사하겠습니다.

다음 글에서는 이 문제에 대한 해결책을 제시해 보고, Code Review 를 끝낸 후에 LogListener.h 랑 LoggingEvent.h 를 구현해 보도록 하겠습니다. (컴파일은 언제 하려나 ...?)

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



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