고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #10 - 로깅 라이브러리 구현하기
그나저나 고절가주팁글이 미디어몹 헤드라인에 한 반나절 걸렸습니다. 안타깝게도 화면 캡쳐하기 전에 내려와서 캡쳐한 이미지는 보여드릴 수 없지만 다음 이미지를 보시면 헤드라인에 걸렸었다는 걸 짐작하실 수 있을 것 같네요.
미디어몹 헤드라인에 걸린 고절가주팁 #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 설계
잘못된 설계 Defect을 하나 수정했네요. 이제 멤버 함수들을 구현해 보도록 하지요.
먼저 subscribe() 와 unsubscribe()! 간단합니다. subscribe() 가 호출되면 list 컨테이너의 push_back() 멤버 함수를 써서 list 에 추가하고, unsubscribe() 가 호출되면 remove() 멤버 함수를 써서 list 에서 삭제하면 됩니다. STL의 list 컨테이너가 이 모든 복잡한 것들을 구현해 놓고 있기 때문에 우리는 그냥 이용만 하면 됩니다.
// 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 제어 코드만 복잡하게 많으니까 상당히 밸런스가 맞지 않는 느낌이 납니다. 어떻게 이 코드를 상당히 엘레강스하면서도 뷰티풀하면서도 우아하게 만들 수 없을까요 ? ......
한참 생각을 해 봤더니 STL의 for_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란 무엇인가를 클릭하세요.