
시작하며
처음 이벤트 기반 프로그래밍을 시도하면 생각보다 어렵게 느껴질 수 있습니다. 이벤트 기반 프로그래밍은 함수 중심의 방식과는 다른 사고방식을 요구하기 때문입니다. 이 글에서는 이벤트 기반 프로그래밍에서 어떤 관점을 가지고 코드를 작성해야 하는지 함께 살펴보겠습니다.
이벤트 기반 사고방식
1. Fire and Forget
이벤트 기반 프로그래밍이 어려운 첫번째 이유는, 우리가 다른 객체의 함수를 호출하고 결과를 확인하는 것에 익숙해져 있기 때문입니다. 그런데 이벤트의 발행은 어떤 사건이 발생했다고 알리는 행위에 불과할 뿐이기에 아무 결과도 알려줄 수가 없습니다. 이벤트는 발행되는 순간 이미 내 손을 떠난 것입니다. 발행하고 잊는 것. 이것이 바로 Fire and Forget입니다. 말로만 들어서는 이해하기 어려울 수 있으니, 간단한 코드를 이벤트 기반 프로그래밍 방식으로 바꾸면서 개념을 살펴보겠습니다.
// 함수 중심 MyClass
class MyClass
{
private:
OtherClass otherObject;
MessengerController messenger;
private:
bool doFirstSubwork() {
// ...
}
public:
bool doWork() {
if(doFirstSubwork() == false) {
messenger.sendAlert("first subwork failed!");
return false;
}
if(otherObject.doSecondSubwork() == false) {
messenger.sendAlert("second subwork failed!");
return false;
}
return true;
}
};
// 함수 중심의 OtherClass
class OtherClass
{
public:
doSecondSubWork() {
// ...
}
};
// 함수 중심의 MessengerController
class MessengerController {
public:
sendAlert(std::string message) {
// ...
}
};
MyClass는 자신이 수행하는 작업과 OtherClass가 수행하는 작업, 총 두 가지 작업을 합니다. 그리고 둘 중 하나라도 실패를 한다면 메신저에 경고 메시지를 보내는 간단한 클래스입니다. 이 클래스를 이벤트 기반 프로그래밍 방식으로 바꾸어 보겠습니다.
우선 내부에서 의존하는 객체 중에 어떤 객체를 이벤트 객체로 바꿀지 판단해야 합니다. messenger 객체는 메신저에 알림을 보낸다는 분명한 고유의 책임을 가지고 있습니다. 따라서 이벤트 객체로 바꿔야 합니다. otherObject 또한 비즈니스 로직 흐름 상 분명한 책임을 가진 객체라고 판단된다면, 이벤트 방식으로 바꾸어야 합니다. 반대로 Json파서와 같이 도구에 가까운 수동적인 객체라고 판단된다면 그대로 놔두면 됩니다.
otherObject가 이벤트 객체라고 판단된 경우를 가정해 보겠습니다. 지난 포스트 "이벤트 객체 작성 원칙"에서 언급했듯이 이벤트 객체에는 public 함수가 존재하면 안됩니다. messenger와 otherObject가 이벤트 객체인 이상, sendAlert나 doSecondSubwork 같은 함수는 호출할 수 없습니다. 호출을 하지 못하니 그 결과도 알 수 없습니다. 이벤트 객체가 할 수 있는 일은 자신의 일을 다 하고, 그 사실을 알리는 것 뿐입니다. 그런 점을 고려하여 코드를 작성한다면 다음과 같이 바뀝니다.
// 이벤트 기반의 MyClass
class MyClass {
private:
std::shared_ptr<EventListener<WorkRequested>> listener;
private:
bool doFirstSubwork() {
// ...
}
public:
MyClass() {
listener =
Event<WorkRequested>::subscribe([this](auto event){
if(doFirstSubwork() == false) {
Event<FirstSubworkFailed>::publish();
return;
}
Event<FirstSubworkDone>::publish();
});
}
};
// 이벤트 기반의 OtherClass
class OtherClass {
private:
std::shared_ptr<EventListener<FirstSubworkDone>> firstSubworkListener;
private:
bool doSecondSubwork() {
// ...
}
public:
OtherClass() {
firstSubworkListener =
Event<FirstSubworkDone>::subscribe([this](auto event){
if(doSecondSubwork() == false) {
Event<SecondSubworkFailed>::publish();
return;
}
Event<SecondSubworkDone>::publish();
});
}
};
// 이벤트 기반의 MessengerController
class MessengerController {
private:
std::shared_ptr<EventListener<FirstSubworkFailed>> firstSubworkFailListener;
std::shared_ptr<EventListener<SecondSubworkFailed>> secondSubworkFailListener;
private:
void sendAlert(std::string message) {
// ...
}
public:
MessengerController() {
firstSubworkFailListener =
Event<FirstSubworkFailed>::subscribe([this](auto event){
sendAlert("first subwork failed!");
});
secondSubworkFailListener =
Event<SecondSubworkFailed>::subscribe([this](auto event){
sendAlert("second subwork failed!");
});
}
};
코드가 길어져서 더 복잡하게 느껴질 수도 있습니다. 우선 MyClass만 집중해서 보도록 합시다. 함수 중심의 MyClass는 신경쓰는 것이 많았습니다. 자신의 작업 뿐만 아니라 OtherClass의 작업 성공 여부와 알람의 전송까지도 MyClass가 신경을 써야 했습니다. 하지만 이벤트 기반 프로그래밍 방식의 MyClass는 자기가 책임진 FirstSubwork 외에 다른 작업을 신경쓰지 않습니다. 작업의 성공/실패 여부를 이벤트를 발행하여 알린 다음, 잊을 뿐입니다. 다른 클래스들도 마찬가지 입니다. OtherClass는 SecondSubwork에 집중하고 MessengerController는 알림 기능에 집중합니다. 자기가 할 일을 다했다면 사실을 알리고 잊으면 됩니다.
2. 사건 중심 사고
위의 예시에서, 함수 중심 코드가 더 짧고 간단해 보일 수 있습니다. 그 이유는 함수 호출이라는 행위에 너무 많은 사건이 함축되어 있기 때문입니다. 어떤 객체가 다른 객체에게 요청을 합니다. 그 요청은 성공할 수도, 실패할 수도 있습니다. 함수 호출이 성공한 경우에는 보통 하나의 작업 뿐만이 아니라 관련된 모든 작업을 성공했다는 것을 뜻합니다. 실패한 경우에도 실패 원인은 한가지가 아니라 여러가지입니다. 어떤 작업이 요청되었다는 것도 어떠한 사건에 해당하므로 이벤트입니다. 큰 작업에 포함되는 작은 작업 하나하나의 성공도 이벤트가 될 수 있습니다. 원인이 다른 제각각의 실패 또한 이벤트가 될 수 있습니다. 함수 호출은 이 많은 맥락을 코드 한 줄로 뭉뚱그려 버립니다. 그래서 얼핏 보기에는 짧고 간단한 코드를 만들어냅니다.
이벤트 기반 프로그래밍 방식으로 바꾼 코드를 보면 이 사실이 명확하게 보입니다. doWork 함수를 호출하여 작업을 요청하는 사건은 WorkRequested 이벤트가 되었습니다. 각 하위 작업 FirstWork와 SecondWork의 성공/실패도 각각 별개의 이벤트가 되었습니다. 함수 호출에 엮인 사건들을 풀어헤치고, 이벤트에 반응하여 새로운 이벤트를 발행하는 방식으로 코드를 고치고 나니 책임의 분리가 더 명확해졌습니다. MyClass는 FirstWork를 책임지고 OtherClass는 SecondWork를 책임지며 MessengerController는 메신저 알림을 책임집니다. 보통 클래스가 다르면 서로 다른 파일에 나뉘어 작성되기 때문에, 메신저 알람이 언제 발송되는지 알고 싶다면 원래는 MyClass나 OtherClass의 코드까지 열어봐야 했을 것입니다. 하지만 이제는 MessengerController의 코드만 보아도 언제 알람이 발송되는지 유추가 가능합니다. 코드는 더 길어졌어도 책임의 분리가 더 명확한 코드라 할 수 있습니다. 이처럼 함수 호출에 함축된 사건과 이벤트를 풀어헤쳐 생각하면 책임을 더 명확하게 분리할 수 있습니다.
3. 진정한 단일 책임
이 글에서 책임이라는 단어를 자주 언급한 것을 느끼셨을 겁니다. "단일 책임 원칙"은 객체지향의 SOLID 원칙에서도 첫번째를 차지할 만큼 매우 중요합니다. 하지만 함수 중심 사고방식으로는 제대로된 단일 책임을 구현하기가 어렵습니다. 다시 한 번 예시를 들어 살펴보겠습니다. 운영중인 서비스에 회원을 더 유치하기 위해 친구를 10명 초대하면 100포인트를 지급하는 이벤트를 진행하게 되었다고 해 봅시다. 기존처럼 함수 중심으로 작성된 코드는 다음과 비슷할 겁니다.
1. 친구 서비스에서 사용자의 친구 초대 횟수를 카운팅
2. 친구 초대 횟수가 10번이 되면 포인트 서비스의 addPoint(userId, 100)와 같은 함수를 호출
객체지향의 원칙을 잘 따랐다면, 단일 책임의 원칙에 따라 아마 친구 서비스는 친구 관련 정보만 잘 관리하고 포인트 서비스는 회원별 포인트만 관리하도록 작성되어 있을 것입니다. 함수 중심적 객체 지향 사고방식에서는 친구 초대와 관련된 데이터가 포인트 서비스에 들어가는건 너무나 어색하게 느껴집니다. 그래서 십중팔구는 위와 같은 코드가 작성될 것입니다. 그 결과로 친구 서비스는 친구 초대라는 핵심 기능 외에 포인트 서비스가 필요로하는 정보까지 가공하여 제공해야 하는 책임이 생겼습니다. 포인트 서비스는 친구 서비스가 시키는 대로 기계적으로 포인트를 더해 줄 뿐입니다.
반면에, 이벤트 기반 프로그래밍 방식에서는 코드가 다음과 같이 바뀝니다.
1. 사용자가 친구를 초대할 때, 친구 서비스는 FriendInvited 이벤트를 발행
2. 포인트 서비스에서 FriendInvited 이벤트에 반응해 사용자별 친구 초대 횟수를 카운팅
3. 친구 초대 횟수가 10이 되면 해당 사용자에게 100포인트를 지급
위에서 살펴본 함수 중심 작업 흐름과의 제일 큰 차이점은 친구 초대 횟수를 포인트 서비스가 카운팅한다는 것입니다. 각 이벤트 객체는 이벤트를 발행할 때, 다른 객체에게 어떤 정보가 언제 필요하게 될지는 전혀 신경쓰지 않습니다. 사건이 일어난 즉시 바로 이벤트를 발행할 뿐입니다. 따라서 각 이벤트 객체는 프로그램에서 발생하는 이벤트들을 적극적으로 수집하고 가공해두어 미리 준비해야 합니다. 데이터가 미리 준비되어 있어야 다른 객체의 도움 없이도 작업을 처리할 수 있습니다. 친구 초대 이벤트가 발생할 때마다 "사용자별 친구 초대 횟수"를 미리 세어 정보를 가공해 두어야 포인트를 적절하게 지급할 수 있는 것입니다. 이렇게 바뀌니, 친구 서비스는 핵심 기능인 친구 초대에만 집중할 수 있게 되었습니다. 포인트 서비스는 포인트를 지급해야 할 조건을 미리 알고, 그 조건을 체크하기 위해 이벤트를 수집하여 데이터를 갱신하며, 조건이 만족되면 포인트를 지급합니다. 포인트 지급 이벤트와 관련된 요구사항을 처음부터 끝까지 모두 책임지게 되는 것입니다. 이것이 진정한 단일 책임입니다.
여전히 친구 초대 횟수 데이터가 포인트 서비스에 존재하는 것이 어색하게 느껴질 수 있습니다. 하지만 데이터가 아니라 책임을 중심으로 생각해야 합니다. 함수 요청이 있으면 시키는대로 포인트를 더해줄 뿐인 포인트 서비스는 사실은 아무 책임도 지지 않습니다. 시키는 대로만 움직이는 병사에겐 패배의 책임을 묻지 않는 것과 같은 이치입니다. 이런 객체는 책임을 자율적인 객체라기보단 버튼을 누르면 움직이는 수동적인 기계나 다름없습니다. 단순히 데이터를 관리한다고 해서 책임을 지는 것이 아닙니다. 포인트 지급과 관련된 정보를 스스로 수집하고 판단하여 행동해야만 포인트 서비스가 포인트를 진정으로 책임진다고 말할 수 있습니다.
마치며
이번 글에서는
이벤트 기반 사고방식에 대하여 이야기해 보았습니다.
이벤트 기반 프로그래밍을 이해하시는데 도움이 되었으면 정말 좋겠습니다.
이벤트 기반 프로그래밍에 관심이 있으시다면
구독하여 이벤트 기반 프로그래밍 시리즈를 지켜봐 주시기 바랍니다.
또한 다른 의견이나 궁금한 점이 있으시다면 댓글로 말씀해주세요.
토의는 언제나 환영합니다.
여기까지 읽어주셔서 감사합니다.