채움의 길
[DeepDive] 시스템이 이벤트를 이해하는 방식 본문
시스템이 이벤트를 이해하는 방식
- EventListener의 동작 원리 : 이벤트 발생 시 트리거되는 비동기 콜백구조 및 이를 통한 결합도 감소
- MessageQueue와의 연계 개념 : Listener가 이벤트를 "구독(consume)"하는 역할을, Queue가 "전달" 역할을 수행
- 요청-응답 vs 이벤트 기반 구조 비교 : 시스템 간 통신 방식의 차이와 그로 인한 확장성/유연성 변화
- 이벤트 처리의 신뢰성 포인트 : Listener의 중복 이벤트 처리(idempotency)와 순서 보장 문제
EventListener의 동작 원리
EventListener는 이벤트가 발생했을 때 실행되는 비동기 콜백 함수로, 주체(이벤트 발생자)와 반응자(Listener)가 직접 연결되지 않아 결합도가 낮아진다는 이점이 있다.
동작 흐름
- 이벤트 흐름 (Subscribe) : Listener가 "이런 이벤트가 생기면 알려줘"라고 시스템에 등록
- 이벤트 발생 (Publish / Emit) : 특정 동작이 일어나면 시스템이 이벤트를 발생
- 리스너 트리거 (Trigger Listener) : 이벤트가 발생하면 해당 이벤트를 구독한 Listener들이 자동으로 호출 (이 호출은 비동기로 처리되어 메인 로직의 흐름을 방해하지 않음)
비동기 콜백 구조
해당 콜백 구조는 '동작 흐름'의 3번 내에서 일어나게 된다.
- 등록 (Register) : Listener가 "콜백 함수" 형태로 시스템에 등록
- 대기 (Listen) : 이벤트 발생을 기다림 (Blocking하지 않음)
- 호출 (Invoke) : 이벤트가 발생하면 시스템이 콜백을 자동 호출
- 후처리 (Async) : 대부분의 Listener는 별도 스레드에서 실행되어 메인 로직과 분리
→ 이 구조 덕분에 이벤트 발생자(Producer)는 Listener의 존재를 몰라도 되고, Listener는 이벤트의 "발생 시점"을 직접 제어할 필요가 없음
결합도 감소의 원리
| 항목 | 기존(보편적인) 구조 | 이벤트 기반 구조 |
| 호출 방식 | 직접 메서드 호출 | 이벤트 발생 후 Listener가 처리 |
| 의존 관계 | 발생자(Producer)가 처리 로직을 알아야 함 | Listener만 등록하면 됨 |
| 확장성 | 새로운 기능 추가 시 코드 수정 필요 | 새로운 Listener만 추가하면 됨 |
즉, 시스템이 '이벤트'만 주고받는 구조이기 때문에 누가 받는지 몰라도 되고, 이로 인해 결합도가 감소하여 확장성이 향상됨
MessageQueue와의 연계
이전 소주제에서 "Producer는 Listener의 존재를 몰라도 된다"라고 언급했다.
MessageQueue는 이 둘의 중간 역할을 해주며 해당 과정을 보다 깔끔하게 처리할 수 있도록 도와준다.
- Producer(이벤트 발행자) : 이벤트를 만들어 Queue에 "보낸다" (publish, emit)
- 비동기화, Producer의 역할은 여기서 끝남
- 결합도 감소, Producer와 Listener가 서로를 몰라도 동작 가능
- Message Queue(전달자) : 이벤트를 "저장하고 전달"한다 (buffer, broker, decoupling)
- 신뢰성, 중간에서 메세지를 보관하므로 장애에도 데이터 유실 방지
- Listener / Consumer(이벤트 수신자) : Queue에 쌓인 메세지를 "구독(consume)"하고 처리한다 (subscribe, process)
- 확장성, Listener를 여러 개 띄워서 부하 분산 가능
MessageQueue와의 연계가 추가되면, Producer와 Listener 사이의 결합도가 더욱 낮아지고 시스템의 확장성이 한층 강화된다.
이벤트 기반 구조는 Producer → Message Queue → Listener(Consumer) 로 이어지는 비동기 이벤트 파이프 라인이며, 이 덕분에 서비스 간 의존성을 낮추고 장애에 확장 가능한 시스템을 만들 수 있다.
요청-응답 vs 이벤트 기반 구조 비교
이번엔 요청-응답 구조와 위에서 다뤄왔던 이벤트 기반 구조를 비교해볼 차례이다.
여기서 중점을 두고 봐야할 부분은 아래 두 가지.
- 서로를 어떻게 인식하는가? (공간적 결합)
- 언제 반응하는가? (시간적 결합)
| 관점 | 요청-응답 구조 | 이벤트 기반 구조 | 변화의 의미 |
| 인식 방식 (공간적 결합) | 호출자는 "대상(Server)"의 위치(API, URL, IP 등)를 직접 알아야 함 | Producer는 Listener의 존재나 위치를 몰라도 됨, 이벤트를 발행할 뿐 | 호출 대상에 대한 의존성 제거 -> 결합도 감소 |
| 반응 시점 (시간적 결합) | 요청과 응답이 동시에 일어나야 함, 둘 다 활성 상태 필요 | 이벤트는 나중에 처리 가능, Queue를 통해 시간적 분리 | 비동기 처리 -> 유연성, 복원력 향상 |
| 통신 매개체 | 네트워크 호출 (HTTP, RPC 등) | Message Queue (Kafka, RabbitMQ 등) | 버퍼링, 재시도, 순서 보장 가능 |
| 장애 대응력 | 상대 서비스가 죽으면 요청 실패 | Queue가 중간 역할을 하여 Listener 복구 시 재처리 가능 | 신뢰성 확보 |
| 확장성 | 요청량 증가 시 서버 부하 급증 | Listener를 여러 개 띄워 소비 병렬화 가능 | 수평 확장 용이 |
| 변화 대응력 | 기능 추가 시 기존 API 수정 필요 | 새로운 Listener 추가만으로 이벤트 확장 가능 | 변화에 유연 |
이벤트 처리의 신뢰성 포인트
그렇다면 비동기로 이루어진, 결합도가 낮아져 느슨하게 연결된 시스템이 "한 번만, 올바른 순서"로 이벤트를 처리하려면 어떻게 해야하는가?
멱등성
: 같은 이벤트가 여러 번 와도 결과는 한 번 처리한 것과 동일해야 한다.
- 해결 방법
- 이벤트 ID 추적 : 이벤트마다 고유 ID 부여 -> 이미 처리한 ID는 무시
- DB 레벨 멱등성 : INSERT IGNORE, ON DUPPLICATE KEY UPDATE 등으로 중복 삽입 방지
- 트랜잭션 내 체크 로직 : 이벤트 처리 전 상태 점검 (조건문 등)
멱등성을 유지하기 위한 핵심 포인트는 "Listener는 이벤트를 받는 순간, 이게 새로운 이벤트인지를 먼저 검증해야 한다는 것"
순서 보장
: 이벤트는 반드시 '의도한 순서대로' 처리되어야 한다.
- 해결 방법
- Partition Key 사용 (Kafka) : 동일한 엔티티 ID 기준으로 같은 파티션에 보내 순서 유지
- Sequence Number 부여 : 이벤트에 시퀀스 번호를 붙여 Listener가 순서 검증 후 처리
- Event Store 기반 재정렬 : 이벤트를 일단 저장 후, 순서대로 처리하는 별도 스케줄러 운영
순서를 보장하기 위한 핵심 포인트는 "애플리케이션 레벨에서 직접 관리해야 할 것"
단, 순서가 달라도 최종 상태만 일관되면 되는 프로세스에서는 "결과적 일관성"을 확보하면 됨
'지식 채우기 > 동아리' 카테고리의 다른 글
| [DeepDive] Circuit Breaker의 목적과 동작 방식 알아보기 (0) | 2026.01.07 |
|---|---|
| [DeepDive] CQRS와 Projection으로 보는 조회 모델의 진화 (0) | 2025.10.14 |
| [DeepDive] 동시성 제어 기법을 알아보자 (2) | 2025.09.27 |