transactional-outbox-pattern.spec

aliases
No value
tags
curinginnos/refactor
description
No value
links
curinginnos
status
ai response
project
false
area
true
resource
false
title
transactional-outbox-pattern.spec
created
2026-03-01T01:36:45
updated
2026-04-05T17:36:01

문제정의

WHY:

현재 시스템은 애그리거트의 상태 변경과 이벤트 저장을 단일 트랜잭션으로 묶는 'Event Store' 방식을 훌륭하게 구현하여 데이터의 원천(Source of Truth)을 확보했습니다. 하지만 이벤트를 EventEmitter2로 발행하는 과정이 동일 트랜잭션 혹은 직후의 메모리 상에서 이루어지고 있어, 애플리케이션 크래시나 리스너 장애 발생 시 **이벤트 발행의 유실(At-Least-Once delivery 실패)**이 발생할 위험이 존재합니다. 이를 해결하고 궁극적으로 Kafka 도입을 위한 안전한 교두보를 마련하기 위해 완벽한 Transactional Outbox Pattern의 완성이 필요합니다.

WHERE:

WHAT:

기존 transactional_outbox에 이벤트 발행 여부를 추적할 수 있는 상태 필드를 추가하고, 발행되지 않은 이벤트를 폴링(Polling)하여 EventEmitter2(향후 Kafka)로 전달한 뒤 상태를 업데이트하는 Event Relay Worker를 구현합니다.

예상 개발기간, 소요시간

IN-SCOPE

model TransactionalOutbox {
  id  String
  eventId String
  aggregateType String
  aggregateId String
  
  payload Json
  metadata Json?
  
  status String
  retryCount Int
  
  publishedAt DateTime?
  lastError String?
  nextAttempAt DateTime?
  
  @@map("transactional_outbox")
}
interface TransactionalOutbox {
  id: string;
  eventId: string;
  aggregateType: string;
  
  payload: Record<string, unknown>;
  metadata?: {traceId?: string; causationId?: string; } & Record<string, unknown>;
  
  status: 'PENDING' | 'PUBLISHED' | 'RETRYING' | 'FAILED';
  retryCount: number;
  
  publishedAt?: Date;
  lastError?: string
  nextAttemptAt?: Date;
}
interface UnitOfWork {
  run<T>(fn: () => Promise<T>): Promise<T>;
}

class PrismaUnitOfWork implements UnitOfWork {
  async run(fn) {
    return this.prisma.$transaction(fn);
  }
}

// some Application Service Layer
await uow.run(async () => {
  await eventStore.appendEvents(events);
  await outbox.enqueueMany(events);
});

OUT-SCOPE

DEPENDENCY

RISK

TIME-ESTIMATED

16hr (스키마 변경, Worker 구현, 컨슈머 멱등성 점검 및 테스트 포함)

마스터리스트 (평가지표 체크리스트)

이 태스크가 완료 상태로 바뀌기 위해 통과하여야 하는 조건들을 나열하는 마스터테이블입니다.

항목명 설명 검증 방법 ⌛️🏃✅❌
Relay Worker 동작 스케줄러가 미발행 이벤트를 정상적으로 가져오는지 확인 로그 및 디버깅을 통한 쿼리 실행 확인 ⌛️
이벤트 발행 및 상태 갱신 이벤트를 Dispatcher에 태운 후 DB 상태가 PUBLISHED로 변경되는지 확인 테스트 코드 (Worker 실행 후 DB 상태 Assert) ⌛️
장애 복구 (Retry) Dispatch 실패 시 이벤트를 유실하지 않고 다음 주기에 재시도하는지 확인 Dispatcher에 의도적 예외 주입 후 재시도 여부 확인 ⌛️
컨슈머 멱등성 보장 동일한 이벤트가 2번 이상 인입되었을 때 시스템 상태가 1번 처리된 것과 동일한지 확인 동일 이벤트 Payload를 연속 2회 발행하여 결과 상태 Assert ⌛️

Usecase Scenarios

UCTRX001: 미발행 이벤트의 릴레이(Relay) 및 상태 업데이트

[액터]

Event Relay Worker (NestJS Background Task)

[전제조건]

[시나리오]

  1. Event Relay Worker가 설정된 주기(예: 2초)마다 실행된다.

  2. transactional_outbox에서 status: PENDING 인 이벤트를 설정된 Batch Size(예: 101건)만큼 시간순으로 조회한다.

  3. 조회된 이벤트를 순회하며 Event Bus(EventEmitter2)로 이벤트를 발행(Dispatch)한다.

  4. 이벤트 발행이 성공하면, 해당 이벤트의 ID를 모아 transactional_outboxstatus: PUBLISHED로 업데이트한다.

[사후조건]

[예외흐름]

UCTRX002 재시도 로직

includes UCTRX001

[액터]

Event Relay Worker (NestJS Background Task)

[전제조건]

적어도 하나의 이벤트가 버스로의 발송에 실패하여 status: RETRYING 으로 전이했다.

[시나리오]

  1. 액터는 UCTRX001 작업이 수행되기 직전 재시도 이벤트를 조회한다.
    1. WHERE status: RETRYING, nextAttemptAt <= 액터가 호출된 시간
    2. SORT eventId
  2. 조회된 이벤트를 순회하며 Event Bus로 이벤트를 발행한다.
  3. 이벤트 발행이 끝나면 해당 이벤트의 status: PUBLISHED로 업데이트한다.

[사후조건]

[예외흐름]

참고자료