이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • event와 state는 같은 주문 데이터를 어떻게 다르게 표현하는가?
  • state만 저장하면 downstream 재처리에서 어떤 정보가 사라지는가?
  • Java/Spring 도메인 코드에서 상태 변경과 event 생성을 어떻게 연결하는가?
  • 취소, 삭제, 보정은 state update와 event log에서 각각 어떻게 다뤄야 하는가?

개요

state는 현재 결과다.

event는 그 결과에 도달한 사건이다.

orders.status = PAID는 state다.

OrderPaid는 event다.

데이터 파이프라인은 현재 state만으로는 과거 사건의 순서와 원인을 복원하기 어렵다.

그래서 검색 색인, 추천 feature, warehouse, audit, 알림은 state snapshot보다 event log를 필요로 하는 경우가 많다.

원리

state는 덮어쓴다.

event는 append 한다.

state는 지금 API가 보여줄 값을 빠르게 알려 준다.

event는 어떤 일이 언제 발생했고 어떤 consumer가 다시 처리할 수 있는지 알려 준다.

둘 중 하나만 선택하는 문제가 아니다.

운영 DB에는 state가 필요하고, downstream 흐름에는 event가 필요하다.

비교

구분StateEvent
질문지금 상태는 무엇인가무슨 일이 있었는가
예시주문 상태 PAIDOrderPaid
변경update 가능append-only 권장
조회현재 API 응답재처리, projection, 분석
위험이력 손실중복, 순서, 호환성
keyaggregate idevent id + aggregate id

state는 현재 업무 정합성을 지키고, event는 데이터 이동과 재처리의 기준이 된다.

Spring 도메인 예시

public class Order {
    private OrderStatus status;
    private final List<DomainEvent> events = new ArrayList<>();
 
    public void pay(Payment payment) {
        if (status != OrderStatus.CREATED) {
            throw new IllegalStateException("payable order required");
        }
        this.status = OrderStatus.PAID;
        this.events.add(new OrderPaid(id, payment.id(), payment.amount(), payment.paidAt()));
    }
}

이 코드에서 status 변경은 state 변경이다.

OrderPaid 추가는 event 생성이다.

transaction commit 후 event가 outbox나 broker로 이동하면 downstream consumer가 같은 사건을 처리할 수 있다.

State만 남겼을 때의 한계

SELECT order_id, status, updated_at
FROM orders
WHERE order_id = 10042;

이 조회는 현재 상태를 알려 준다.

하지만 주문이 생성, 결제, 취소, 재결제 중 어떤 경로를 거쳤는지는 알기 어렵다.

updated_at 하나로는 어떤 consumer가 어떤 사건부터 재처리해야 하는지도 알 수 없다.

Event Log 예시

{
  "eventId": "evt-10042-002",
  "eventType": "OrderPaid",
  "aggregateType": "Order",
  "aggregateId": "10042",
  "occurredAt": "2026-06-30T10:15:30+09:00",
  "eventVersion": 1,
  "payload": {
    "paymentId": "pay-555",
    "paidAmount": "49000.00",
    "currency": "KRW"
  }
}

이 event는 어떤 주문에서 어떤 결제가 승인되었는지 알려 준다.

consumer는 eventId로 중복을 막고, aggregateId로 같은 주문의 처리 순서를 유지할 수 있다.

취소와 삭제

state에서는 취소가 status = CANCELED로 보인다.

event log에서는 OrderCanceled가 새로 append 되어야 한다.

삭제도 마찬가지다.

downstream 저장소에서 검색 문서를 제거해야 한다면 OrderDeleted 또는 OrderArchived 같은 사건이 필요하다.

state에서 row가 사라지기만 하면 consumer는 무엇을 지워야 하는지 모른다.

판단 축

  • 현재 API 응답에는 state를 사용한다.
  • downstream projection, 검색 색인, warehouse 재처리에는 event를 사용한다.
  • event 이름은 상태명보다 과거형 사건으로 짓는다.
  • state 변경과 event 생성은 같은 transaction 경계에서 누락 없이 연결한다.
  • 취소와 삭제도 별도 event로 표현해 consumer가 복구할 수 있게 한다.

실패 모델

  • OrderStatusChanged 하나로 모든 의미를 처리해 consumer가 business rule을 추측한다.
  • state update는 성공했지만 event가 누락되어 warehouse가 늦게 반영된다.
  • row delete만 수행해 검색 색인과 mart에서 삭제 사실이 전파되지 않는다.
  • event 없이 최종 state snapshot만 보내 과거 순서를 복원할 수 없다.
  • event를 재발행했을 때 consumer가 중복 처리된다.

운영 지표

  • state_event_mismatch_count: state 변경 대비 event 누락 수다.
  • outbox_pending_count: 아직 발행되지 않은 state 변경 사건 수다.
  • event_duplicate_count: 같은 event id가 다시 들어온 수다.
  • projection_lag_seconds: event 발생과 downstream state 반영 사이 지연이다.
  • delete_event_missing_count: 삭제나 취소 전파 누락을 찾는 지표다.

개인 프로젝트 기준

개인 프로젝트에서는 event sourcing까지 구현하지 않아도 된다.

운영 table과 별도로 order_event table을 두고 state 변경 때 append 하는 것만으로도 충분하다.

중요한 것은 state update와 event append를 같은 transaction에서 처리하고, event id와 처리 이력을 남기는 것이다.

기업 운영 기준

기업에서는 state와 event의 owner와 보관 정책이 다를 수 있다.

운영 DB state는 서비스 팀이 책임진다.

event log는 여러 consumer와 데이터 팀이 사용하므로 schema 변경과 보관 기간, replay 권한을 따로 관리한다.

실전 팁

  • event 이름을 정할 때 “무슨 상태인가”가 아니라 “무슨 일이 발생했는가”로 묻는다.
  • row delete가 필요한 기능은 downstream 삭제 event도 함께 설계한다.
  • state와 event의 timestamp 의미를 구분한다.
  • event id는 consumer 중복 방어의 기본 key로 쓴다.
  • state snapshot event가 필요하면 그것이 사건인지 snapshot인지 이름에 드러낸다.

위험 신호!

  • status 값 변경만으로 모든 비동기 처리를 설명한다.
  • event payload에 현재 state 전체만 담고 발생 원인이 없다.
  • 취소와 삭제가 downstream으로 전달되지 않는다.
  • state 변경과 event 발행이 서로 다른 transaction에서 조용히 실패한다.
  • consumer가 event id 없이 aggregate id만으로 중복을 판단한다.

확인 질문

확인 질문

  • orders.status = PAIDOrderPaid의 차이는 무엇인가?
    • 전자는 현재 상태이고, 후자는 결제가 발생했다는 사건이다.
  • state만 저장하면 재처리에서 어떤 정보가 부족한가?
    • 사건 순서, 발생 시각, event id, 취소/삭제 원인, consumer가 다시 처리할 기준이 부족하다.
  • 삭제 기능을 event 관점에서 설계할 때 무엇을 남겨야 하는가?
    • 어떤 aggregate가 언제 삭제 또는 보관 처리되었는지 나타내는 별도 event와 처리 key를 남겨야 한다.

참고 문서