Development

4. 이벤트 객체 작성 원칙 - [이벤트 기반 프로그래밍 시리즈]

성난소 2025. 3. 22. 00:57
목차
1.시작하며
2.이벤트에 반응하는 이벤트 객체
2.1.원칙1: public 멤버 변수와 함수 절대 금지
2.2.원칙2: 같은 클래스의 다른 객체 접근 금지
3.마치며

 

시작하며

지난 포스트에서는 이벤트의 조건을 알아보고
간단한 이벤트 시스템을 구현해 보았습니다.
이벤트 시스템은 옵저버 패턴으로 구현할 수 있었습니다.
이벤트 종류의 식별은 강타입 언어의 타입 시스템을 이용하면 편리합니다.
이벤트 객체는 이벤트를 자유롭게 구독하고
이벤트에 대한 반응 또한 스스로 정할 수 있어야 합니다.
때문에 이벤트 시스템은 전역적이어야 합니다.

이번에는 이벤트 시스템을 이용하여
간단한 이벤트 객체를 직접 구현하면서,
이벤트 객체를 작성할 때의 원칙과 팁을 이야기해 보려고 합니다.


이벤트에 반응하는 이벤트 객체

업계에 널리 퍼진 정의는 아니지만,
이벤트 객체가 무엇인지 간단하게 다시 되짚어봅시다.
이벤트 기반 프로그래밍의 개념 포스트에서 말씀드렸듯
이벤트 객체는 이벤트에 반응하며 스스로를 완벽히 책임지는 객체입니다.
다른 객체의 명령, 즉 함수 호출을 통해 동작하는 객체가 아니라
반응할 이벤트를 스스로 선택하고 필요하다면 또 다른 이벤트를 발행하는 객체입니다.
이벤트 객체는 어떻게 작성할 수 있을까요?

 

원칙1: public 멤버 변수와 함수 절대 금지

객체지향에서도 public 변수는 일반적으로 권장되지 않습니다.
대신 public함수와 인터페이스를 적극적으로 활용합니다.
하지만 이벤트 객체는 함수 호출을 통해 동작하지 않으므로 public 멤버 함수 또한 필요하지 않습니다.
시리즈 첫 포스트에서 public 멤버 함수가 가지고 있던 많은 단점들을 고찰해보았습니다.
언급했듯이, public 멤버 함수가 있으면 다른 함수에서의 호출을 절대 거부할 수 없습니다.
따라서 public 멤버 함수를 단순히 선언하는 것 만으로도 스스로에 대한 완벽한 제어권을 상실하게 됩니다.
정말 단순하게 private 멤버 변수 값을 반환하는 getter 함수일 뿐이라도 마찬가지입니다.
getter 함수의 시그니처를 전혀 수정하지 않더라도, 클래스내에서 해당 private 멤버 변수를 수정하는 타이밍을 바꾸려 한다면 그 변수의 getter 함수를 호출하는 모든 코드를 살펴봐야 될 지도 모릅니다.
public 멤버 함수는 스스로에 대한 책임과 제어권을 포기하고 다른 객체들에게 떠넘긴다는 것을 명심하세요.
이벤트 객체가 독립적이고 자율적인 객체가 될 수 있으려면 public 함수를 선언해선 안됩니다.
이벤트 객체가 다른 이벤트 객체와 상호작용 할 때에는 꼭 이벤트를 통해야만 합니다.

절대 사용해선 안되는 public 함수와 달리, private 함수는 얼마든지 사용해도 됩니다.
private 함수는 외부에서 접근할 수 없어서 제어권을 포기하는 효과가 없기 때문입니다.
오히려 private 함수는 클래스 내부에서 반복적으로 사용되는 코드의 재활용을 통해 코드를 깔끔하게 유지할 수 있도록 도와줍니다.
함수의 원래 탄생 목적인 "코드의 재활용"에만 사용되기 때문에 어울리는 역할을 하는 것이기 때문입니다.
public 함수를 수정할 때에는 호출 스택을 따라 소스코드 전체를 훑어보게 되는 경우도 있습니다만,
private 함수는 현재 수정하고 있는 클래스 내부만 파악하면 충분합니다.

 

원칙2: 같은 클래스의 다른 객체 접근 금지

public 함수를 사용하지 않고 private 함수만을 사용하면 다른 클래스에서 내부 정보에 접근하는 것을 차단할 수 있습니다. 하지만 같은 클래스의 다른 객체가 접근하는 것 까지는 막지 못합니다. 예를 들면 다음과 같은 코드입니다.

class Sample
{
private:
  void notToDoThis(Sample& other) {/*...*/}
}

 

위 코드의 notToDoThis 함수는 다른 객체를 입력으로 받아 해당 객체를 변경하려 합니다.
이벤트 객체가 완전히 독립적이고 자율적이어야 한다는 사실을 생각하면 이 또한 허용되어선 안됩니다.
독립성과 자율성은 객체에게 부여된 것이지 클래스에 부여된 것이 아니기 때문입니다.
같은 "사람"이라는 이유로 다른 사람의 명령에 무조건 응답해야 하는 의무는 없는 것과 같은 이치입니다.
이런 코드는 프로그래밍 문법으로 완전히 막을 수 없기 때문에 컨벤션으로서 지켜져야 합니다.

 


이벤트 객체 예시

위의 원칙만 지키면, 이벤트 객체를 작성하는 것은 어렵지 않습니다.
지난 포스트에서 구현한 이벤트 시스템을 이용해서 간단한 이벤트 객체를 작성해보도록 하겠습니다.
요구 사항은 다음과 같습니다.

1. 회원 가입하면 100 보너스 포인트 제공
2. 매일 첫 로그인시 10 보너스 포인트 제공

 

위와 같은 요구사항이라면,
이벤트는 두가지를 정의할 수 있습니다.
"회원 가입" 이벤트와 "로그인" 이벤트 입니다.
두 이벤트를 코드로 나타내면 다음과 같습니다.

// 회원 가입 이벤트
struct MemberJoined
{
  std::string uuid;
  std::string name;
  std::string email;
  //...
};

// 회원 로그인 이벤트
struct MemberLogined
{
  std::string uuid;
  std::string sessionId;
  //...
};

 

위 두 이벤트를 회원 관리를 담당하는 이벤트 객체에서 발행한다고 가정해 봅시다.
회원 가입 절차가 성공적으로 마무리되면 MemberJoined 이벤트가 발행되고
서비스에 가입된 회원이 로그인을 할 때마다 MemberLogined 이벤트가 발행될 것입니다.
이때, 요구사항에 따라 보너스 포인트를 담당하는 또 다른 이벤트 객체를 작성하기로 한다면, 코드는 다음과 같은 모습이 됩니다.

class BonusPointManager
{
private:
  // 이벤트 리스너들
  std::shared_ptr<EventListener<MemberJoined>> memberJoinListener;
  std::shared_ptr<EventListener<MemberLogined>> memberLoginListener;
  
  // 멤버 데이터 변수들
  std::map<std::string, uint64_t> pointByUuid;
  std::map<std::string, Time> lastLoginTimeByUuid;

private:
  // 내부 로직을 위한 함수들
  bool isFirstLoginToday(const std::string& uuid)
  {
    auto lastLoginTime = lastLoginTimeByUuid[uuid];
    auto now = Time::now();
    
    return now.day() != lastLoginTime.day(); 
  }
  
  void addPoint(const std::string& uuid, int point)
  {
    pointByUuid[uuid] += point;
  }
  
public:
  // 생성자에서 이벤트를 구독하고 반응 행동을 정의합니다.
  BonusPointManager() {
    // 회원 가입 이벤트 리스너 정의
    memberJoinListener =
      Event<MemberJoined>::subscribe([this](auto event){
        lastLoginTimeByUuid[uuid] = Time::now();
    	pointByUuid[uuid] = 100;
        
        Event<BonusPointUpdated>::publish({
          uuid,
          pointByUuid[uuid]
        });
      });
  
    // 회원 로그인 이벤트 리스너 정의
    memberLoginListener =
      Event<MemberLogined>::subscribe([this](auto event){
        if(!isFirstLoginToday(event.uuid))
          return;
        
        addPoint(uuid, 10);
        
        Event<BonusPointUpdated>::publish({
          uuid,
          pointByUuid[uuid]
        });
      });
  }
}
C++에는 위와 같은 Time 클래스가 없습니다.
가독성을 위해 가상의 클래스를 사용하였습니다.

 

BonusPointManager의 코드를 살펴보면,
원칙대로 public 함수의 정의가 없기 때문에 외부에서 조작이 불가능한 객체입니다.
pointByUuid와 같은 내부 정보는 외부에서 변경할 수 있는 방법이 전혀 없습니다.
생성자에서 이벤트 리스너를 만들며 정의한 함수의 내용대로, 스스로 이벤트에 반응하여 변경하는 방법밖에 존재하지 않습니다.
정말 스스로를 온전히 책임지게 되는 것입니다.
이와 같은 구조는 클래스 내부 구현을 더욱 수정하기 쉽게 만들어 줍니다.
private 멤버 변수라도 외부로부터의 함수 요청이 있으면 변경될 가능성이 있었던 기존 함수 중심의 방식과는 달리, 멤버 변수가 변경될 시점과 그 방식도 외부의 참견 없이 스스로 결정하기 때문입니다.

또한 BonusPointManager 클래스는 보너스 포인트가 변경될 때, BonusPointUpdated 이벤트를 발행하는 것을 볼 수 있습니다.
이는 자신이 수행한 작업의 결과를 발생한 사건, 즉 이벤트의 형태로 프로그램 내의 다른 이벤트 객체가 관측 가능하게끔 해주는 것입니다.
하위 객체들에게 어떤 함수를 호출해야 할지 일일이 지정해줬던 기존 함수 객체와는 달리 이벤트 객체는 그저 자신이 유발한 사건을 알림으로서 그 임무를 다 합니다.
실제 프로그램에서는 이런 모양의 이벤트 객체 수십, 수백개가 협력하여 프로그램 전체의 목적을 달성하게 됩니다.

 


마치며

이번 글에서는
이벤트 객체의 작성 원칙을 알아보고
간단한 이벤트 객체 코드 예시를 소개했습니다.

 

너무 간단한 예시라 독자 분들에게 정말 와닿을지 걱정이 됩니다.
혹시 애매한 점이 있다면 댓글로 꼭 알려주세요.
성심성의껏 답해드리겠습니다.

 

이벤트 기반 프로그래밍에 관심이 있으시다면
구독하여 이벤트 기반 프로그래밍 시리즈를 지켜봐 주시기 바랍니다.
또한 다른 의견이나 궁금한 점이 있으시다면 댓글로 말씀해주세요.
토의는 언제나 환영합니다.

여기까지 읽어주셔서 감사합니다.

'Development'의 다른글

  • 현재글 4. 이벤트 객체 작성 원칙 - [이벤트 기반 프로그래밍 시리즈]

관련글