무엇을 했다는 이벤트를 발행하고 관심 있는 쪽이 받아 처리합니다. 결합도가 낮아지지만 전체 흐름이 코드 한 곳에 안 보여 추적이 어려워집니다.
이벤트 기반 아키텍처
핵심 개념
- 이벤트 발행(Publish): 서비스가 상태 변경 시 이벤트 발행
- 이벤트 구독(Subscribe): 관심 있는 서비스가 이벤트 수신
- 서비스 간 직접 호출 없이 느슨한 결합 유지
패턴
| 패턴 | 설명 | 복잡도 |
|---|
| Pub/Sub | 발행-구독 | 낮음 |
| Event Sourcing | 이벤트 자체를 저장 | 높음 |
| CQRS | 읽기/쓰기 모델 분리 | 중간 |
무엇이 어려워지는가
결합도가 낮아지는 대신 세 가지가 어려워집니다. 도입하기 전에 이것을 감수할 수 있는지 봅니다.
전체 흐름이 코드 한 곳에 없습니다. 주문 뒤에 무엇이 일어나는지 알려면 그 이벤트를 받는
쪽을 다 찾아야 합니다. 직접 호출이면 함수를 따라가면 보입니다. 그래서 이벤트를 쓰면 누가
무엇을 받는지 적어 두는 문서나 추적 도구가 필수가 됩니다.
순서가 보장되지 않습니다. 주문됨과 결제됨이 순서대로 발행돼도 받는 쪽에 도착하는 순서는
다를 수 있습니다. 순서가 중요하면 같은 대상의 이벤트를 한 줄로 모으거나, 받는 쪽이 순서와
무관하게 같은 결과를 내도록 만들어야 합니다.
이벤트가 계약입니다. 필드 하나를 지우면 그것을 읽던 모든 쪽이 깨집니다. 그런데 누가
읽는지 발행하는 쪽은 모릅니다. 그래서 필드를 더하는 것은 값싸고 지우거나 뜻을 바꾸는 것은
비쌉니다. 지울 때는 안 쓰는 것을 확인한 뒤 지웁니다.
언제 쓰지 않는가
| 상황 | 왜 |
|---|
| 결과를 바로 알려줘야 한다 | 이벤트는 언제 처리될지 약속하지 않는다 |
| 받는 쪽이 하나뿐이고 늘 그럴 것이다 | 직접 부르는 것이 읽기 쉽다 |
| 실패를 부른 쪽이 책임져야 한다 | 이벤트는 실패를 받는 쪽에 남긴다 |
서비스가 둘인데 이벤트를 쓰는 것은 대개 이릅니다. 결합도를 낮춰 얻는 이득보다 흐름이
안 보이는 비용이 큽니다.
실무 포인트
- 주문 → 결제 → 배송: 각 단계를 이벤트로 연결
- 이벤트 유실 방지: Outbox 패턴 (DB+이벤트 원자적 저장)
- 디버깅 어려움 → 이벤트 추적(Tracing) 도구 필수
Q.이벤트 드리븐 아키텍처란?
서비스가 서로를 직접 부르지 않고 일어난 사실을 알리고, 관심 있는 쪽이 반응하는 구조입니다.
| 항목 | 명령 방식 | 이벤트 방식 |
|---|
| 메시지 성격 | 이것을 해달라 | 이런 일이 있었다 |
| 아는 대상 | 누구에게 시키는지 안다 | 누가 듣는지 모른다 |
| 추가 | 부르는 쪽을 고쳐야 한다 | 구독자를 새로 붙이면 된다 |
| 흐름 파악 | 코드를 따라가면 보인다 | 흩어져 있어 어렵다 |
주문이 생겼을 때를 비교하면 이렇습니다.
| 방식 | 주문 서비스가 하는 일 |
|---|
| 명령 | 재고 차감을 호출하고, 알림 발송을 호출하고, 적립을 호출한다 |
| 이벤트 | "주문 생성됨" 을 발행한다. 나머지는 각자 구독한다 |
두 번째에서 적립 기능을 추가할 때 주문 서비스를 고치지 않는다는 것이 이 구조의 값입니다.
| 대가 | 내용 |
|---|
| 흐름이 보이지 않는다 | 누가 무엇에 반응하는지 코드에 안 나온다 |
| 결과적 일관성 | 즉시 반영되지 않는다 |
| 디버깅 | 트레이싱 없이는 추적이 어렵다 |
| 계약 관리 | 이벤트 형식을 바꾸면 모르는 구독자가 깨진다 |
흔한 실수: 이벤트에 명령을 담는 것. "재고를 차감하라"는 이벤트가 아니라 명령입니다. 이벤트는 이미 일어난 사실을 과거형으로 알리고, 무엇을 할지는 받는 쪽이 정합니다.
Q.동기 vs 비동기 통신 트레이드오프는?
| 항목 | 동기 | 비동기 |
|---|
| 결과 확인 | 즉시 | 나중에. 별도 경로 필요 |
| 지연 | 상대의 시간이 내 응답에 더해진다 | 내 응답은 빠르다 |
| 장애 전파 | 상대가 느리면 나도 느려진다 | 상대가 죽어도 메시지는 남는다 |
| 부하 완충 | 없다. 그대로 전달된다 | 큐가 받아 둔다 |
| 오류 처리 | 실패를 바로 알려준다 | 재시도와 실패 큐를 설계해야 한다 |
| 흐름 파악 | 쉽다 | 어렵다 |
| 일관성 | 즉시 | 결과적 |
판단 기준은 사용자가 그 결과를 지금 알아야 하는가입니다.
| 상황 | 선택 |
|---|
| 결제 승인 | 동기. 결과 없이는 다음 화면을 못 만든다 |
| 주문 확정 후 알림 | 비동기. 지금 알 필요가 없다 |
| 재고 차감 | 동기. 실패하면 주문을 받을 수 없다 |
| 통계 집계 | 비동기 |
한 요청 안에서 섞는 것이 보통입니다. 꼭 필요한 것만 동기로 처리하고 나머지를 큐로 넘기면, 응답은 빠르고 실패 지점은 줄어듭니다.
흔한 실수: 비동기가 더 빠르다고 답하는 것. 전체 처리 시간은 오히려 늡니다. 짧아지는 것은 사용자가 기다리는 구간입니다.
Q.이벤트 소싱(Event Sourcing)이란?
현재 상태를 저장하지 않고 변경 사건을 순서대로 저장하는 방식입니다. 상태는 그 사건들을 재생해서 만듭니다.
| 방식 | 저장하는 것 |
|---|
| 일반 | 잔액 = 8,000원 |
| 이벤트 소싱 | 입금 10,000 → 출금 3,000 → 입금 1,000 |
| 얻는 것 | 내용 |
|---|
| 완전한 이력 | 언제 왜 바뀌었는지 전부 남는다 |
| 과거 상태 재현 | 특정 시점으로 되돌려 볼 수 있다 |
| 감사 | 금융이나 회계에서 요구하는 추적성 |
| 사후 분석 | 나중에 필요해진 집계를 과거 데이터로 만들 수 있다 |
| 대가 | 내용 |
|---|
| 조회가 어렵다 | 현재 상태를 얻으려면 재생해야 한다 |
| 스냅샷 필요 | 사건이 많아지면 중간 상태를 저장해 둬야 한다 |
| 이벤트 변경 불가 | 저장된 사건은 못 고친다. 보정 사건을 추가한다 |
| 학습 비용 | 팀 전체가 이 모델을 이해해야 한다 |
그래서 전면 도입보다 이력과 감사가 중요한 일부 영역에만 쓰는 편이 현실적입니다. 잔액 변동, 주문 상태 변화 정도입니다.
흔한 실수: 이벤트 드리븐 아키텍처와 같은 것으로 보는 것. 전자는 저장 방식이고 후자는 서비스 간 통신 방식입니다. 각각 따로 쓸 수 있습니다.
Q.CQRS 패턴을 설명해주세요
쓰기 모델과 읽기 모델을 분리하는 패턴입니다. 같은 데이터를 다루지만 구조를 따로 둡니다.
| 측면 | 쓰기 모델 | 읽기 모델 |
|---|
| 최적화 대상 | 정합성과 규칙 검증 | 조회 속도 |
| 구조 | 정규화 | 화면에 맞게 미리 합쳐 둔 형태 |
| 저장소 | 관계형 DB | 검색 엔진, 캐시, 별도 테이블 |
분리하는 이유는 두 요구가 충돌하기 때문입니다. 쓰기는 정규화가 유리하고, 조회는 조인 없이 한 번에 읽는 편이 빠릅니다. 하나의 모델로는 둘 다 만족하기 어렵습니다.
동기화는 이벤트로 합니다. 쓰기가 일어나면 이벤트를 발행하고 읽기 모델을 갱신합니다. 그래서 읽기 모델은 항상 조금 뒤처집니다.
| 대가 | 내용 |
|---|
| 결과적 일관성 | 쓴 직후 조회에 반영되지 않을 수 있다 |
| 복잡도 | 모델이 둘, 동기화 경로가 하나 늘어난다 |
| 정합성 점검 | 두 모델이 어긋났는지 확인할 절차가 필요하다 |
흔한 실수: 읽기와 쓰기를 분리하니 무조건 도입하는 것. 조회 요구가 특별히 복잡하지 않으면 대가만 남습니다. 그리고 읽기 전용 복제본을 쓰는 것과 혼동하기 쉬운데, 복제본은 같은 모델의 사본이고 CQRS 는 모델 자체가 다릅니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
메시징 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.