C++이야기 스물아홉번째: Portable C++ Timer Class #2

Posted at 2008. 12. 22. 22:35 // in S/W개발/C++ 이야기 // by 김윤수


If you are an English speaker, then follow this link.
C++ Tips 29th: Portable C++ Timer Class #2

이번에는 timer 관련 클래스들에 어떤 멤버 함수와 멤버 변수를 정의해야할지에 대해 자세히 알아보겠습니다. 사용자가 가장 많이 사용하게될 클래스는 TimerDriver일테니 이것부터 시작해 보도록 하겠습니다.

저번글에서 말씀드렸던 것처럼 TimerDriver는 등록된 타이머들이 정확한 시간에 expire되도록 해주는 클래스입니다. 이점을 고려하면 타이머 인스턴스들을 등록하고 등록해제하는 멤버 함수는 누가 봐도 필요할 것 같습니다.

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);
};

이 인터페이스를 정의하고 나면 등록된 타이머 인스턴스들을 저장해 놓을 저장 공간이 필요하다는 걸 금방 알게 됩니다. 그럼 어떤 데이터 구조가 저장 공간으로 적당할까요? 가장 적당한 데이터 구조를 택하려면 아무래도 사용자가 TimerDriver를 사용하는 패턴을 먼저 알아야 할 것 같습니다.

지금까지 정의한 멤버 함수는 RegisterTimer(), UnregisterTimer() 두 개가 있고, 이 둘은 결국 삽입과 삭제에 해당합니다. 그리고 타이머 인스턴스들 하나 하나를 건드리면서 드라이브 할 수 있습니다. 그렇지만 사용자가 정렬된 타이머 리스트를 필요로 하거나, 타이머 인스턴스를 직접 접근하거나, 타이머 인스턴스를 상당히 빠르게 검색해야할 필요성은 없을 것입니다.

그래서 저는 std::list가 등록된 타이머 인스턴스 저장 데이터 구조로 가장 적당하다고 생각합니다.

#include <list>

class Timer;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer> timer_list_;
};

다음으로 RegisterTimer() 와 UnregisterTimer()의 입력 인자와 리턴값에 대해 생각해 보겠습니다.

제가 RegisterTimer()의 리턴값과 UnregisterTimer()의 입력인자로 TimerId라는 타입을 도입했습니다. 'TimerId' 타입의 필요성은 분명해 보이는데... 어떤식으로 'TimerId' 타입을 정의해야할까요? TimerDriver가 내부적으로 식별자 풀(identifier pool)을 관리해야할까요 아니면 TimerId가 그냥 Timer*이면 될까요? 식별자 풀을 관리한다면 각 타이머 인스턴스에 할당된 식별자는 어디에 저장해야할까요? timer_list_는 list<Timer>라고 선언해도 무방할까요 아니면 다른 뭔가로 선언해야할까요? 예를 들어, list<Timer*>와 같이 말이죠. 우리가 결론에 도달할 때까지는 이런 수 많은 질문들이 머리속을 맴돌게 됩니다. 혼란 그 자체죠? 이런 간단한 클래스를 설계하는데도 말입니다.

그렇지만, 소프트웨어 개발자로서 선택에 대한 분명한 이유를 찾아가며 이런 혼란스러움을 해결해 가야 합니다. 삶이란 게 문제로 가득차 있고, 산다는 것 자체가 해답을 찾아가는 과정 아닐까요? 그러니 너무 괴로워하지 마시고 맘을 가다듬어 보시죠. 사실 이 모든 문제들이 서로 연결되어 있습니다. 한 문제에 대한 해결책을 찾게 되면... 짠! 하고 다른 모든 문제들에 대한 해결책도 찾을 수 있게 될테니까요.

그럼 문제점을 하나씩 해결해 보시죠. 숨을 기~~~~~~~~~~~~~피 들이 쉬시고~~~

TimerId! TimerDriver가 식별자 풀을 관리해야할까요? 가능한 해결책이죠. 사용자가 RegisterTimer()를 호출하여 타이머 인스턴스를 등록해달라고 하면 TimerDriver가 식별자 풀에서 할당되지 않은 ID를 새로 등록할 타이머 인스턴스에 할당할 수 있을 것입니다. 그리고, 나중에 UnregisterTimer()를 호출했을 때, 삭제하려면 할당된 그 ID를 어딘가에 저장해야 합니다.

그런데 저는 정말 식별자 풀을 관리하고 싶지는 않네요. 현재 요구사항에 비해 너무 복잡한 해결책이거든요. 저는 간단한 해결책을 훨씬 선호합니다. 그렇게 하면 코드량도 적어지고, 버그고 적어질 테니까요. 그래서 식별자 풀은 설계 대안에서 제외하기로 했습니다. 그럼 다른 대안이 있을까요? TimerId를 그냥 Timer*로 선언하면 어떨까요?

#include <list>

class Timer;
typedef Timer* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer> timer_list_;
};

이렇게 하면 식별자 풀을 관리하지 않아도 되고, RegisterTimer() 는 리턴값으로 &t를 리턴하면 되니 구현이 간단해 질 것입니다. 그럼, 거의 완벽한 해결책을 찾은 거네요. 그죠? 워!워!워! 그렇게 쉽게 해결될 리가 있나요. 조금만 더 깊이 생각해 보시죠.

그냥 TimerId를 Timer*로 선언하게 되면 타이머 인스턴스의 내부 표현 방식을 사용자가 직접 다루게 되고, 결국 이것은 encapsulation rule을 위배하게 됩니다. 그렇게 할 경우, TimerDriver가 타이머 인스턴스 내부 표현 방식을 바꾸게 되면 TimerId가 Timer*라는 거를 가정하고 짠 사용자 코드는 다 작동하지 않게 될 것입니다. 그래서 저는 TimerId를 그냥 void*로 선언하려고 합니다. 그럼 사용자는 타미어 인스턴스 내부 표현 방식이 어떻게 되는지 전혀 알 수가 없겠지요.

#include <list>

class Timer;
typedef void* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer> timer_list_;
};

다음으로는 timer_list_ 멤버 변수에 대해 생각해 보겠습니다. 저번 글에서 밝혔다시피 저는 Timer에서 상속받은 PeriodicTimer와 OneShotTimer를 정의하려고 합니다. Timer에는 공통의 인터페이스만 정의하고 PeriodicTimer 와 OneShotTimer에서 그걸 구현하여 다형성을 보이게 하려는 것이지요. 그렇다면 timer_list_를 list<Timer*>로 선언해야 할 것입니다.

#include <list>

class Timer;
typedef void* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer*> timer_list_;
};

현재 TimerDriver 인터페이스에는 한 가지 멤버 함수가 빠진 것 같습니다. 보통 사용자는 먼저 타이머들을 등록한 후에 타이머들을 돌리기 시작하겠죠? 그래서 다음과 같이 Run() 멤버 함수를 추가했습니다.

#include <list>

class Timer;
typedef void* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);
    void Run();

private:
    std::list<Timer*> timer_list_;
};

이제, 이 정도면 TimerDriver의 인터페이스는 거의 완벽하다고 느껴지네요. 다음글에서 TimerDriver의 구현에 대해 다뤄보도록 하겠습니다.