본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Backend

이벤트 기반 아키텍처 설계

작성 2026. 11. 1.추가
목차14개 항목
출처와 참고

「회원시스템 이벤트기반 아키텍처 구축하기」, 「이벤트 기반 분산 시스템을 향한 여정」, 「신규 포인트 시스템 전환기 #1」, 「배달의민족 가게노출 시스템을 소개합니다」, 「실시간 댓글 개발기(part.1)」을 바탕으로 재구성한 노트다. 메시지와 이벤트의 구분, 요청-응답과의 비교, 브로커 이중화, 이벤트 불변성과 정정은 「fru1tworld/cs-basic 모의 면접 질문 목록」이 묻는 개념을 채워 넣었다. karanpratapsingh의 「System Design」 강의 저장소를 바탕으로 생산자·라우터·소비자 구성과 잘 맞는 쓰임을 보강했다.

이벤트 기반 아키텍처란

이벤트 기반 아키텍처(event-driven architecture)는 시스템이 「무슨 일이 일어났다」는 사실을 이벤트로 발행하고, 관심 있는 다른 시스템이 그 이벤트를 구독해 각자 반응하는 구조다. 발행자는 누가 무엇을 하는지 모른다.

모듈 사이에 직접 호출이 늘면 기능 하나를 붙일 때마다 기존 코드를 고쳐야 하고, 순환 의존이 생기고, 모듈 경계가 무너진다. 이벤트는 이 호출 사슬을 끊는 수단이다.

구성 요소는 셋이다. 이벤트 생산자(event producer)가 이벤트를 발행하고, 이벤트 라우터(event router, 브로커·이벤트 버스)가 이벤트를 걸러 소비자에게 밀어 주며, 이벤트 소비자(event consumer)가 이벤트를 받아 자기 상태에 반영한다. 생산자는 소비자를 모르고 소비자끼리도 서로 모른다. 구현 패턴으로는 발행·구독, 이벤트 소싱과 CQRS, 사가가 있다.

잘 맞는 곳은 메타데이터·지표 수집, 서버·보안 로그 모으기, 서로 다른 기술로 된 시스템 잇기, 하나의 사건을 여러 곳에서 병렬로 처리하는 팬아웃이다.

시스템을 물리적으로 나누거나 호출을 비동기로 바꾸는 것만으로는 결합이 줄지 않는다. 「회원 탈퇴 후 쿠폰을 회수해 줘」라는 메시지를 비동기로 보내도, 발행자가 쿠폰 시스템의 할 일을 알고 있으면 논리적 의존은 그대로다. 결합을 끊는 것은 메시지의 의도다.

메시지, 이벤트, 커맨드

메시지(message) 는 시스템 사이에 오가는 전달 단위다. 봉투(헤더, ID, 타입)와 본문으로 이뤄지고, 브로커가 큐나 토픽으로 나른다. 메시지가 무엇을 담느냐에 따라 커맨드와 이벤트로 나뉜다. 「메시지는 지시, 이벤트는 사실」이라는 말은 이 구분을 줄인 것이다. 엄밀히는 둘 다 메시지이고, 커맨드는 받는 쪽에게 행동을 지시하는 메시지, 이벤트는 이미 일어난 사실을 알리는 메시지다.

구분이벤트(event)커맨드(command), 비동기 요청
담는 것이미 일어난 사실 (회원이 탈퇴했다)상대에게 바라는 행동 (쿠폰을 회수하라)
이름과거형명령형
수신자0명 이상. 발행자는 모른다정해진 대상 하나
수신자가 바뀌면발행자는 그대로발행자도 고쳐야 한다
거절이미 일어났으므로 거절할 수 없다. 수신자는 무시하거나 반응할 뿐이다수신자가 검증해 거절할 수 있다
책임발행자는 사실을 알리는 것까지. 무엇을 할지는 수신자가 정한다보내는 쪽이 결과를 기대하고, 실패를 알아야 한다
어울리는 전달토픽·이벤트 스트림(여러 구독자, 다시 읽기)큐(한 처리자, 처리 후 삭제)

메시지에 수신 측의 할 일이 들어가 있으면 이벤트 모양을 한 커맨드다. 큐와 이벤트 스트림의 구조 차이는 메시지 큐와 Kafka에 있다. 도메인의 핵심 행위가 무엇인지 잘 보이지 않으면 이벤트 스토밍으로 도메인 이벤트부터 찾는다(도메인 주도 설계(DDD)).

발행자가 수신자를 모른다는 것

이점비용
구독자를 더하고 빼도 발행자 코드는 그대로다「이 이벤트를 누가 쓰나」를 코드에서 알 수 없다. 이벤트 카탈로그·추적이 따로 필요하다
구독자가 죽거나 느려도 발행자는 영향을 받지 않는다구독자가 처리에 실패해도 발행자는 모른다. 실패를 구독자 쪽에서 감시해야 한다
팀마다 독립적으로 배포한다이벤트 스키마가 여러 팀의 공개 계약이 되어 함부로 못 바꾼다
같은 사실로 새 기능을 계속 붙인다이벤트가 연쇄로 이어지면 전체 업무 흐름이 아무 데도 보이지 않는다

이벤트의 세 계층

같은 「이벤트」라도 범위에 따라 역할과 설계 규칙이 다르다.

계층범위하는 일페이로드
앱 이벤트한 프로세스 안, 프레임워크 기능도메인 행위와 그 뒤의 부가 정책을 코드 수준에서 분리한다자유
내부 이벤트한 도메인(시스템) 안주 행위 뒤에 붙는 부가 정책(알림, 이력 기록)을 처리한다구독자 처리에 필요한 만큼 넉넉히
외부 이벤트시스템 사이다른 도메인에 사실을 알린다최소한으로 닫아 둔다
  • 주 행위와 부가 정책을 이벤트로 나누면 도메인의 응집도가 올라가고, 부가 정책이 바뀌어도 주 행위 코드는 그대로다.
  • 앱 이벤트 리스너는 트랜잭션의 어느 단계에서 실행할지(커밋 전·커밋 후)를 고를 수 있다. 부가 작업을 같은 트랜잭션에 넣을지, 커밋 뒤로 미룰지를 도메인 코드를 건드리지 않고 정한다.
  • 프로세스 안에서 쓰는 발행기와 외부 메시징은 릴레이 컴포넌트로 잇는다. 발행·구독 인터페이스로 메시징 기술을 감싸 두면 브로커를 바꾸기도 쉽다.

페이로드 설계

외부 이벤트는 수신 측의 업무 변화에 끌려다니지 않게 작게 만든다. 수신 측마다 필요한 필드를 넣어 주기 시작하면 발행자가 수신 측의 요구를 알게 되고, 결합이 다시 생긴다.

방식담는 것장점비용
일반화된 최소 형식식별자, 행위, 속성, 발생 시각여러 시스템이 같은 형식으로 필요한 이벤트만 골라 쓴다수신 측이 추가 정보를 따로 구해야 할 수 있다
영 페이로드(zero-payload)식별자와 행위만결합이 가장 적다. 수신 측이 최신 상태를 API로 다시 읽으므로 순서가 뒤바뀌어도 결과가 맞는다수신마다 조회 API 호출이 생긴다
상태 포함(event-carried state)변경된 데이터 전체수신 측이 조회 없이 처리한다스키마 변경이 모든 구독자에 퍼진다

이벤트는 봉투(envelope) 모양으로 감싼다. 발생 시각, 이벤트 타입, 원본 데이터를 함께 담아 역직렬화와 라우팅에 쓴다. 타입 정보를 보고 클래스를 골라 역직렬화하는 방식은 편하지만, 임의 타입을 만들 수 있게 열어 두면 호환성과 보안 문제가 생기므로 허용 목록으로 제한한다.

이벤트 스키마는 생산자와 여러 소비자 사이의 계약이므로, 바꿀 때는 하위 호환을 지킨다. 기본값 있는 필드만 추가하고, 이름·타입·의미는 바꾸지 않고, 깨지는 변경은 새 이벤트 타입·버전으로 낸다. 호환성 종류와 스키마 레지스트리는 데이터 관리 패턴의 스키마 진화에 있다.

메시징 브로커가 구독 필터(메시지 속성으로 거르기)를 지원하면, 구독자는 필요한 이벤트만 받고 애플리케이션에서 분류할 부담이 준다.

발행 시점과 정합성

도메인 데이터 저장과 이벤트 발행을 어떻게 맞추느냐가 가장 어려운 문제다.

발행 시점문제
트랜잭션 안에서 발행메시징 장애가 곧 도메인 기능 장애가 된다. 롤백돼도 이벤트는 이미 나갔을 수 있다
커밋 뒤에 발행커밋과 발행 사이에 프로세스가 죽으면 이벤트가 사라진다
트랜잭셔널 아웃박스도메인 데이터와 같은 DB, 같은 트랜잭션으로 이벤트를 저장하고 발행 여부를 표시한다. 별도 프로세스(배치·CDC)가 미발행 이벤트를 다시 보낸다

아웃박스의 동작은 데이터 관리 패턴: CDC, 이벤트 소싱, CQRS, 최종 일관성에 있다. 발행한 이벤트를 모두 저장해 두면 조건별 재발행, 사용자 활동 추적, 흩어진 이력 테이블의 통합에도 쓸 수 있다. 대신 그 저장소의 쓰기·읽기 부하를 감당할 확장 계획이 필요하다.

전달 모델과 소비자

모델동작쓰임
큐(경쟁 소비자)메시지 하나를 소비자 하나가 처리한다작업 분산, 워커 확장
토픽(발행·구독)구독자 모두가 같은 메시지를 받는다여러 시스템에 사실 전파

같은 역할의 서버 여러 대가 토픽을 구독하면 서버마다 같은 이벤트를 처리하는 중복이 생긴다. 그래서 구독 그룹 단위로는 토픽, 그룹 안에서는 큐처럼 동작하는 모델(Kafka 컨슈머 그룹, ActiveMQ Virtual Topic, SNS → SQS 팬아웃)을 쓴다(메시지 큐와 Kafka).

소비자 쪽에서 지킬 것은 다음과 같다.

  • 멱등하게 처리한다. 대부분의 브로커는 최소 한 번(at-least-once) 전달이라 같은 메시지가 다시 온다. 이벤트 ID로 처리 여부를 기록하거나, 결과가 같아지는 연산으로 만든다(장애 확산을 막는 복원력 패턴).
  • 순서는 공짜가 아니다. 순서 보장과 처리량은 맞바꾼다. 순서가 보장되지 않는 큐에서 선행 데이터가 아직 없으면 몇 번 재시도한 뒤 데드 레터 큐(DLQ) 로 보내 따로 처리한다.
  • 원격 호출을 메시지로 바꾸면 시간적 결합이 사라진다. 받는 쪽 DB가 잠시 죽어도 메시지는 쌓였다가 복구 후 처리된다. 대신 즉시 결과가 필요한 작업(중복 사용을 막아야 하는 차감 등)은 동기로 남긴다.

브로커가 단일 장애점이 되지 않게

모든 이벤트가 브로커를 지나므로 브로커가 멈추면 시스템 전체가 멈춘 것처럼 보인다. 여러 겹으로 막는다.

층방법
브로커 자체클러스터로 띄우고 데이터를 복제한다. Kafka는 복제 계수 3과 min.insync.replicas=2, RabbitMQ는 Raft로 복제하는 쿼럼 큐. 노드를 가용 영역(AZ)마다 흩는다
메타데이터·조율Kafka KRaft 컨트롤러처럼 과반 쿼럼으로 두어 노드 하나가 죽어도 리더를 다시 뽑는다(Kafka 운영)
생산자아웃박스로 이벤트를 DB에 먼저 저장하면 브로커가 죽어도 업무 트랜잭션은 성공하고, 이벤트는 복구 뒤 나간다. 생산자 클라이언트의 버퍼·재시도도 짧은 중단을 흡수한다
소비자오프셋·ack로 어디까지 처리했는지 남기므로 브로커가 돌아오면 이어서 처리한다
리전리전 장애까지 견뎌야 하면 다른 리전 클러스터로 복제한다(Kafka MirrorMaker 2, 관리형 서비스의 리전 간 복제)
저하 운영꼭 즉시 필요한 흐름(결제 승인)은 동기 호출로 남기고, 부가 흐름(알림, 통계)만 이벤트에 태워 브로커 장애가 핵심 기능을 멈추지 않게 한다

이벤트를 기록하는 데이터 모델

금전·포인트처럼 이력이 중요한 데이터는 수정·삭제 없이 행을 추가만 하는(insert-only) 원장 모델이 잘 맞는다.

  • 적립·사용·만료·취소를 모두 새 행으로 쌓는다. 잔액은 합계이거나 따로 관리하는 스냅샷이다.
  • 차감과 만료를 적립 건 단위로 추적하는 상세 테이블을 두면 「어느 적립분에서 얼마가 빠졌나」 계산이 단순해진다.
  • 누락이나 오류는 기존 행을 고치지 않고 보정 행을 추가해 바로잡으므로 변경 이력이 그대로 남는다.

이 생각을 극단까지 밀면 상태 대신 이벤트 자체를 저장하는 이벤트 소싱이 된다(데이터 관리 패턴: CDC, 이벤트 소싱, CQRS, 최종 일관성).

불변성과 정정

이벤트는 이미 일어난 사실이므로 고치거나 지우지 않는다(immutability).

불변이어야 하는 이유내용
여러 소비자가 이미 읽었다발행 뒤 고치면 소비자마다 옛 값과 새 값을 섞어 갖게 되고, 누가 무엇을 봤는지 알 수 없다
재생과 재현같은 이벤트를 다시 재생하면 같은 결과가 나와야 조회 모델을 다시 만들고 장애를 재현할 수 있다
감사「언제 무엇이 기록됐나」가 그대로 남아야 감사 기록이 된다
동시성바뀌지 않는 데이터는 잠금 없이 어디서나 복제·캐시·병렬 처리할 수 있다

과거 이벤트가 틀렸으면 새 이벤트로 바로잡는다.

상황정정 방법
값이 틀렸다 (금액 오입력)원 이벤트를 취소하는 역분개(reversal) 이벤트와 올바른 값의 새 이벤트를 차례로 발행한다. 회계 장부가 지우개 대신 정정 분개를 쓰는 것과 같다
차이만 보정하면 된다차액만큼의 보정(adjustment) 이벤트를 낸다(PointAdjusted -500)
이벤트 자체가 잘못 발행됐다OrderPlacedRetracted처럼 무효화 이벤트를 내고, 소비자는 원 이벤트 ID를 보고 반영을 되돌린다
처리 로직이 틀렸다이벤트는 그대로 두고 로직을 고친 뒤 조회 모델을 재생해 다시 만든다

정정 이벤트에는 원 이벤트 ID와 사유를 담아 「무엇을 왜 고쳤나」가 기록에 남게 한다. 개인 정보 삭제처럼 정말로 지워야 하는 경우는 데이터 관리 패턴의 crypto-shredding을 쓴다.

조회 모델 분리와 재처리

여러 원천 시스템의 데이터를 화면 하나에 모아 보여 줘야 할 때, 조회할 때마다 원천을 호출하면 원천 장애가 조회 장애로 번진다. 대신 원천의 변경 이벤트를 구독해 조회 전용 저장소를 서비스 관점으로 다시 만든다(CQRS식 읽기 모델).

flowchart LR
  src["원천 시스템들"] -->|"변경 이벤트"| h["이벤트 처리기"] --> st["조회 전용 저장소"] --> api["조회 API"]
  re["재처리 배치<br/>(전체 재적재 / 특정 시점 이후 / 개별 건)"] --> h
  • 조회 경로는 쓰기 쪽 시스템을 직접 부르지 않는다. 한쪽 장애가 다른 쪽으로 퍼지지 않는다.
  • 이벤트는 유실되거나 잘못 처리될 수 있다. 전체 재적재, 특정 시점 이후 재갱신, 개별 데이터 갱신 세 가지 재처리 경로를 처음부터 열어 두면 장애 복구와 정합성 보정이 쉽다.
  • 처리 성공·실패·건너뜀 같은 도메인 지표를 표준 메트릭에 더해 두면 이벤트 흐름이 눈에 보인다.

기존 시스템에 영향 없이 새 기능 붙이기

트래픽이 큰 기존 시스템에 새 기능(예: 실시간 갱신)을 붙일 때도 이벤트가 유용하다. 기존 시스템은 이미 내던 이벤트를 그대로 발행하고, 새 기능은 그 이벤트를 구독하는 독립된 경로로 만든다. 새 기능이 무너져도 기존 기능(읽기·쓰기)은 영향을 받지 않는다. 역할은 가능한 한 잘게 나눈다. 모듈 하나가 하는 일이 적을수록 장애 원인을 찾기 쉽다.

flowchart LR
  s["기존 서비스"] -->|"이벤트"| b["브로커"] --> p["메시지 가공기"] --> ch["Pub/Sub 채널"] --> rt["실시간 서버들"] --> c["클라이언트"]

실시간 서버 쪽 설계는 실시간 웹소켓 서버 설계에 있다.

요청-응답과 비교한 장단점

비교요청-응답(request-response)이벤트 기반
흐름호출자가 상대를 지정해 부르고 결과를 기다린다발행자가 사실을 알리고, 구독자가 각자 반응한다
결합호출자가 상대의 주소·API·가용성에 묶인다발행자는 브로커와 이벤트 형식만 안다
결과즉시 성공·실패를 안다결과를 바로 모른다. 필요하면 결과 이벤트를 다시 구독한다
장애 전파상대가 죽으면 호출자도 실패한다(연쇄 장애)구독자가 죽어도 이벤트는 쌓였다가 복구 뒤 처리된다
부하호출량이 그대로 상대에게 간다브로커가 버퍼가 되어 소비자는 자기 속도로 처리한다
일관성한 흐름 안에서 강하게 맞추기 쉽다최종 일관성
추적·디버깅호출 스택과 트레이스로 따라간다흐름이 흩어져 분산 추적과 이벤트 카탈로그가 필요하다

장점 셋: 서비스끼리의 결합이 줄어 독립적으로 배포·확장하고, 장애가 번지지 않고 버퍼링되며, 같은 이벤트로 새 기능을 기존 코드 수정 없이 붙인다.

단점 셋:

  • 흐름이 코드에 드러나지 않는다. 이벤트가 어디서 나와 어디로 가는지 문서·추적 도구 없이는 파악하기 어렵다.
  • 최종 일관성을 받아들여야 한다. 사용자에게 「방금 한 일」이 바로 보여야 하는 화면은 따로 설계한다.
  • 중복, 유실, 순서 뒤바뀜, 스키마 호환을 모두 소비자와 운영 쪽에서 다뤄야 한다.

마이크로서비스 사이 통신을 고를 때도 이 비교가 기준이다. 호출자가 결과를 받아야 다음으로 갈 수 있으면(재고 확인 후 주문, 로그인) 동기 API로, 다른 서비스가 알기만 하면 되는 사실(주문 완료 → 알림·적립·통계)이면 이벤트로 보낸다. 오래 걸리는 작업은 비동기 요청-응답(접수 후 상태 조회)으로 뗀다(분산 시스템의 통신·데이터 흐름 패턴).

정리

  • 결합을 끊는 것은 비동기가 아니라 이벤트의 의도다. 사실을 알리고, 할 일은 구독자가 정한다.
  • 이벤트는 앱·내부·외부 계층으로 나누고, 외부 이벤트의 페이로드는 작게 닫아 둔다.
  • 저장과 발행의 정합성은 트랜잭셔널 아웃박스로 맞추고, 아웃박스와 브로커 복제로 브로커가 단일 장애점이 되지 않게 한다.
  • 이벤트는 불변이다. 틀린 사실은 역분개·보정 이벤트로 바로잡는다.
  • 소비자는 멱등하게, 순서는 필요한 만큼만, 실패는 DLQ와 재처리 경로로 다룬다.
  • 잘 나눈 이벤트 경계는 나중에 서비스를 떼어 낼 때의 경계가 된다.

더 읽을거리: 「분산 시스템에서 데이터를 전달하는 효율적인 방법」(우아한테크 영상)

관련 노트: 분산 트랜잭션: 2PC와 사가, Kafka 운영: 복제, 컨슈머 그룹, 튜닝, 모니터링, 데이터 관리 패턴: CDC, 이벤트 소싱, CQRS, 최종 일관성, 메시지 큐와 Kafka, 마이크로서비스 아키텍처, 도메인 주도 설계(DDD), 장애 확산을 막는 복원력 패턴, 실시간 웹소켓 서버 설계

원본 기록7

댓글

GitHub 계정으로 댓글을 남길 수 있어요.