고절가주팁 #9 - 로깅 라이브러리 상세 설계 및 구현 쪼~금

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


고절가주팁 아홉번째 글입니다. 이번 글부터는 제목 형식을 조금 바꾸렵니다. "고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁"이라고 제목을 풀어 썼던 이유는 제 나름대로 이 시리즈의 브랜드(너무 거창한가요 ? ㅋㅋㅋ)를 알려서 고정 독자들을 확보하기 위한 것이었는데, 제 생각에 어느 정도 고정 독자가 확보됐다고 판단했기 때문입니다(어떤 근거로 그러냐구요 ? 제 블로그 RSS 피드 구독자가 그 사이 131명으로 늘었습니다. ^^). 그리고 풀어서 쓴 제목만으로는 독자들이 내용을 짐작하기 어려워서 새로운 독자들을 끌어들이는 건 어려울 것 같더군요.

서론이 너무 길었네요 이만 각설하고, 이번에는 LogSource 를 상세하게 설계해 보고 구현도 쪼금 해보도록 하겠습니다. 그나저나 저랑 같아 잘 가고 계시죠 ? 요즘 또 갈수록 저 혼자 가는 건 아닌가라는 생각이 들어서 그렇습니다. 혼자가면 너무 외로워요. 같이 가요~ 흑흑흑 (로깅 라이브러리 관련된 글을 처음 보시는 분들은 본문에 나와 있는 로깅 라이브러리를 클릭해 보시면 이전 글들을 모두 보실 수 있습니다).

LogSource 부터 시작해 보겠습니다. 지난 글까지 설계했던 내용을 정리하자면 LogSource 의 인터페이스는 다음과 같을 것입니다.

사용자 삽입 이미지

LogSource 인터페이스

그림 처럼 LogSource 는 LogPublisher 의 인터페이스를 구현하기로 했었습니다. LogSource(module, enable, level) 와 같은 생성자가 있어야 할 것이구요. 일반 멤버 함수로는 isEnabled(level), log(pLogEv), log(file, func, line, msg) 같은 것들이 있어야겠지요. 아참 저번에 프로그래밍 인터페이스를 설계할 때는 로깅 수준의 타입을 따로 정의하지 않았는데요. 고절가주팁 #7 에서 설계 방향 중에

2. 로깅 수준은 DEBUG, TRACE, INFO, WARNING, ERROR, FAULT 로 나눔
이런 것이 있었습니다. 그러니 level 의 타입을 그냥 int 로 하면 안되겠지요. 다음과 같이 LogLevel 이라는 enumeration 을 따로 정의해야 할 것입니다.

사용자 삽입 이미지

LogLevel 정의

인터페이스는 그렇다 치고, LogSource 는 어떤 멤버 변수를 가져야 할까요 ? 우선 모듈명을 기억하고 있어야 할 것이고, 로깅 활성화 여부랑 로깅 수준 등도 당연히 기억하고 있어야 할 것입니다. 뭐 두 말하면 잔소리입니다. ^^ 그 외에 기억하고 있어야 하는 건 없을까요 ? 잘 아시다시피 컴퓨터 프로그램은 우리가 기억하라고 하는 것만 기억할 수 있기 때문에 기억해야할 것은 빠짐없이 알려줘야 합니다. 그냥 대강 넘어가는 법이 없지요. 힌트하나 드릴까요 ? LogSource 가 구현하고 있는 LogPublisher 인터페이스를 한 번 보시기 바랍니다(잠깐 생각한 번 해 보시고...).
.
.
.
.
.
.
그렇습니다. 자신에게 subscribe한 LogListener 목록을 가지고 있어야 하겠죠. 별것도 아닌 걸 가지고 괜히 퀴즈 같은 걸 내고 그랬네요 ^^

이상 생각했던 것을 UML 로 표현해 본다면 다음과 같이 나타낼 수 있을 것입니다.

사용자 삽입 이미지

LogSource 클래스 정의

(LogSource 의 인터페이스로 module() 이라는 멤버 함수를 추가했습니다. 고절가주팁 #8에서 LoggingEvent 인터페이스 설계할 때 언급이 됐었습니다) 이 정도로 LogSource 의 설계를 UML 로 표현하는 것은 충분한 것 같으니 실제 코딩을 통해 나머지 설계와 코딩을 함께 진행해 보겠습니다. 코딩을 하면서도 구체적인 설계를 계속하게 될 것입니다. 저는 솔직히 설계를 완벽하게 모델링 툴을 통해 할 수 있다는 걸 별로 믿지 않는 사람입니다. 항상 코딩을 하다보면 뭔가 상세 설계에서 빠진 내용이 있기 마련이고, 그러다 보면 코딩하면서 설계를 추가적으로 하게 되는데... 보통은 그 내용이 상세 설계 문서에 반영되지 않곤 하죠. 그래서 저는 UML 과 같은 모델링 툴은 구조설계시에 사용하는 게 더 적절한 것 같고, 상세 설계 내용은 실제 코딩 자체에 반영되는 것이 낫다고 생각합니다.

그리고, 요즘 javadoc 이나 doxygen처럼 소스코드 안에 documentation 을 넣는 방식도 많이 사용되고 있죠. 저는 이런 방식을 무척이나 좋아라합니다. 왜냐면 프로그래머가 굳이 documentation 을 위해 별도의 툴을 사용할 필요가 없거든요. 그냥 코딩하다가 documentation할 게 생각나면 그냥 소스코드 안에 그 내용을 집어 넣는 거죠. 솔직히 이런 방식이 형상 관리-S/W 프로젝트의 모든 산출물(소스 코드 포함 모든 문서들)을 관리-하는 측면에서도 보면 훨씬 효율적입니다. 산출물간의 consistency 를 유지하기가 무척 좋거든요. 여러분도 꼭 써보시기를 추천합니다.

그래서 LogSource 설계에 대해 UML 을 쓰는 건 이 정도에서 멈추고 코딩을 통해 나머지 설계를 계속해 보도록 하겠습니다. 한마디로 코딩과 설계를 mixup 하면서 작업하는 거죠. 잘 정의된 문서는 코딩 끝난 후에 하겠습니다. ㅋㅋㅋ(이런 식으로 하는 건 워낙 제 성격이 급해서 그런 걸 수도 있습니다. 이런 방식이 불편하신 분들은 꼭 따라 하실 필요는 없습니다. 제 경험에서 우러나온 것이니... 한계가 있을 수도 있죠)

자 그럼... LogPublisher 인터페이스부터 *구현*해 보죠.

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

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

struct LogListener;             // forward declaration

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

}

#endif  // LOGPUBLISHER_H

간단한 LogPublisher 인터페이스를 구현하면서 몇 가지 제가 설계 과정에서 빼 먹었던 것을 *설계*했네요.

1. 클래스별로 별도로 헤더 파일을 둔다.
2. 로깅 라이브러리의 namespace 는 YSLog 로 한다.

이 두 가지 설계 사항은 코드 자체에서 워낙 명확하게 드러나니 별도로 documentation 을 남기진 않아도 될 것입니다.

자~ 그 다음에는 LogSource 를 구현해 볼까요 ?

#ifndef LOGSOURCE_H
#define LOGSOURCE_H

#include <list>
#include <string>
#include "LogPublisher.h"
#include "LogListener.h"
#include "LoggingEvent.h"

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

UML 설계시에는 string 타입으로 표현했던 인자들을 대부분 const char* 로 바꾸었습니다. 왜냐면 프로그래머가 실제 사용할 프로그래밍 인터페이스는 LogSource 의 인터페이스보다는 YSLOG_XXX 와 같은 매크로 함수가 주로 사용될 것 같은데, 거기에서 사용되는 대부분의 인자가 문자열 리터럴일 것이기 때문입니다(C++ 에서는 Java 와 달리 문자열 리터럴이 string 으로 컴파일되는 것이 아니라 const char* 로 컴파일되는 건 다 아시죠 ?). 그리고, 생성자도 설계시에는 항상 세 가지 인자를 꼭 명시하도록 했으나 enable 인자와 lvl 인자는 default 값을 각각 true, YSLog::WARN 으로 설정해서 모듈명만 인자로 명시하더라도 LogSource 인스턴스를 생성할 수 있도록 했습니다. 프로그래머의 편의성을 위한 배려라고 생각하시면 될 것 같습니다.

그리고, isEnabled(), module(), log() 등은 모두 내부 상태를 바꾸지 않기 때문에 const 를 멤버 함수 선언 뒤에 붙여 주었습니다. 고쳐서는 안된다라는 설계 내용을 코딩을 통해 명확하게 나타낸 것이죠. 어디서 주워 들은 말이 생각 나네요 "코드 자체가 문서이다"

LogSource 멤버 함수들은 어떻게 구현해야할까요 ? 그건 다음 시간까지 여러분에게 숙제로 내기로 하겠습니다.

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

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