Skip to content

feat: 이벤트 유실 방지와 재처리 Outbox 기반 구현 - #50

Merged
hywznn merged 9 commits into
mainfrom
agent/25-reliability-outbox
Jul 26, 2026
Merged

feat: 이벤트 유실 방지와 재처리 Outbox 기반 구현#50
hywznn merged 9 commits into
mainfrom
agent/25-reliability-outbox

Conversation

@hywznn

@hywznn hywznn commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

왜 필요한가요?

업무 DB 변경은 commit됐지만 Server가 직후 종료되거나 handler가 실패하면 후속 처리가 사라질 수 있습니다. 같은 이벤트가 다시 전달될 때 업무 결과가 중복 생성되는 문제도 막아야 합니다.

Closes #25

무엇이 바뀌나요?

  • V7에 event_publication, event_consumption과 tenant-aware FK·상태 CHECK·handler unique 제약을 추가했습니다.
  • 업무 transaction 안에서만 동작하는 DomainEventPublisher와 최소 allow-list payload를 구현했습니다.
  • lease 만료 복구, 지수 backoff, 최대 시도, REVIEW_REQUIRED, handler별 멱등 완료 기록을 구현했습니다.
  • Task 생성·취소가 각각 TaskCreated, TaskCancelled를 업무·감사로그와 같은 transaction에서 발행합니다.
  • backlog·oldest delay·completed/retry/review_required Micrometer 지표를 추가했습니다.
  • Outbox 설정, 새 event·handler 작성법, 장애 확인과 복구 기준을 문서화했습니다.

리뷰 반영

  • Outbox 테스트의 DB CURRENT_TIMESTAMP와 JVM Clock 경계를 제거하고, 재처리 시각을 같은 Clock 기준의 확실한 과거로 설정했습니다.
  • Worker·WorkerDocument의 Java 나노초 시각을 PostgreSQL TIMESTAMP(6) 정밀도에 맞추고 updatedAt >= createdAt을 보장했습니다.
  • heartbeat가 없는 현재 구조에서 handler·외부 API timeout은 lease보다 짧아야 한다는 운영 기준과 장시간 작업 도입 조건을 문서화했습니다.
  • REVIEW_REQUIRED 운영 재처리는 권한·사유·버전 검증·감사로그를 포함한 후속 #54로 분리했습니다.
  • RLS Outbox claim bootstrap은 #34와 RLS 가이드에서 이어서 처리합니다.

안전성

  • Event에는 event_id, company_id, request_id, version과 기능별 허용 업무값만 저장합니다.
  • 여권·외국인등록·전화·계좌번호 field, JWT·token·비밀번호·Prompt와 민감 형식 값을 차단합니다.
  • 실패 로그에는 payload·예외 원문을 남기지 않고 안전한 error code만 기록합니다.
  • handler DB 변경과 완료 receipt는 같은 transaction이므로 실패하면 함께 rollback됩니다.

검증

  • 로컬 ./gradlew clean test 통과
  • 관련 Outbox·Worker·WorkerDocument·DB 시각 정밀도 테스트 통과
  • GitHub Actions PostgreSQL 17 migration·계약 검증 통과
  • GitHub Actions Test and build 통과

후속 작업

롤백

V7이 적용된 뒤에는 migration 파일을 삭제하거나 수정하지 않습니다. 문제가 있으면 OUTBOX_ENABLED=false로 worker만 중지하고 publication을 보존한 뒤 forward migration 또는 후속 코드로 교정합니다.

krestar
krestar previously approved these changes Jul 24, 2026

@krestar krestar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 기본 lease 30초는 현재 단계의 초기값으로 적절해 보입니다.
    환경변수로 조정할 수 있고, 아직 장시간 실행되는 실제 handler가 없기 때문에 이번 PR의 차단 사유는 아니라고 생각합니다.
    다만 현재 heartbeat가 없으므로 실제 handler와 외부 API timeout은 lease보다 짧게 제한해야 합니다.
    향후 30초를 넘는 handler가 추가된다면 lease 연장 또는 heartbeat 도입을 함께 검토해야 합니다.

  2. REVIEW_REQUIRED 수동 재처리 API는 별도 운영 Issue로 분리하는 것이 적절합니다.
    재처리에는 운영자 권한, 재처리 사유, AuditLog, expected version 또는 상태 검증이 필요하므로 Outbox 기반 구현과 분리하는 편이 안전합니다.
    별도 Issue를 생성·연결하고, 그전에는 DB를 직접 수정하지 않는 현재 runbook을 유지하면 될 것 같습니다.

  3. Outbox claim bootstrap은 #34에서 RLS 활성화 전 필수 선행조건으로 반영하겠습니다.
    claim 단계에서는 여러 사업장의 payload를 조회하지 않고 제한된 방식으로 event_id, company_id와 lease에 필요한 최소 정보만 반환해야 합니다.
    claim 이후 payload 조회, handler 실행, 완료·실패 처리는 해당 이벤트의 company_id를 SET LOCAL app.company_id로 설정한 transaction 안에서 일반 RLS 정책을 따르도록 하겠습니다.
    Runtime role에 BYPASSRLS, table owner 또는 전체 Outbox 조회 권한은 부여하지 않겠습니다.

위 조건들은 후속 운영 Issue와 #34에서 이어서 처리할 수 있으므로 제 리뷰 범위에서는 이번 PR을 막는 사항은 없습니다.

로컬 PostgreSQL 17.10 환경에서 PR #50 전체 테스트를 실행했습니다.
PostgreSQL 관련 검증은 정상 통과했습니다.

PostgreSqlMigrationTests: 통과
Flyway V1~V7 validate: 통과
AuthRefreshPostgreSqlConcurrencyTest: 통과
PostgreSqlTenantDatabaseContextTest: 6개 전부 통과

다만 전체 clean test에서는 4건이 실패했습니다.

Outbox 테스트 2건은 makeImmediatelyClaimable()에서 next_attempt_at = CURRENT_TIMESTAMP로 설정한 직후 JVM Clock으로 claim 여부를 판단하면서 시간 경계 차이로 processAvailable()이 0을 반환한 것으로 보입니다. 테스트에서 next_attempt_at을 확실한 과거로 설정하거나 동일한 Clock을 사용하는 방향을 확인 부탁드립니다.

Worker 테스트 2건은 로그상 updatedAt must not be before createdAt 예외로 500이 발생했습니다.
DB TIMESTAMP(6)과 Java Instant의 정밀도 차이 또는 현재 시각의 비단조성 문제로 보여, timestamp 정밀도 정규화나 updatedAt >= createdAt 보정이 필요해 보입니다.

이 부분은 PR #50의 Outbox 변경과 직접적인 관련은 없어 보입니다.
따라서 V7 PostgreSQL migration은 정상으로 확인됐지만, 전체 테스트의 로컬 재현 안정성은 한 번 확인 부탁드립니다.

@hywznn

hywznn commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author
  1. 다만 현재 heartbeat가 없으므로 실제 handler와 외부 API timeout은 lease보다 짧게 제한해야 합니다.

이거 고려해보겠습니다 아니면 먼 이야기가 될 것 같지만 그거까지 고려해서 구현해보고 이 부분은 사용법이나 내용에 대해서 문서정리 하겠습니다 블로그쓰듯이

@hywznn
hywznn marked this pull request as ready for review July 25, 2026 02:39
@hywznn
hywznn requested a review from krestar July 25, 2026 02:39

@krestar krestar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시간 경계와 DB timestamp 보정, lease 운영 기준 및 #54 분리까지 확인했습니다.
#34의 tenant-safe Outbox claim bootstrap은 제가 이어서 반영하면 될 것 같네요.

@hywznn

hywznn commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

시간 경계와 DB timestamp 보정, lease 운영 기준 및 #54 분리까지 확인했습니다. #34의 tenant-safe Outbox claim bootstrap은 제가 이어서 반영하면 될 것 같네요.

맞아요 나중에 실제 요청이랑 디비에서 인식된 시간이랑 다르게 될 때 두 번 요청된다든지 아니면 둘이 다른 요청이라고 인식한다든지 그건데 일단 해보라길래 해봤습니다 그러면서 피그마도 정리 중이니 빠르게 확정되고 api도 확정나지기를 ..

@hywznn
hywznn merged commit 2e2f2f5 into main Jul 26, 2026
4 checks passed
@hywznn
hywznn deleted the agent/25-reliability-outbox branch July 26, 2026 00:49

@chaeliki chaeliki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 요청 늦게 확인해서 머지 이후에 남깁니다.
timestamp 정밀도 이슈랑 REVIEW_REQUIRED 분리(#54) 판단 다 좋아 보이고,
따로 막을 부분은 없었을 것 같습니다.

#13(document) 쪽에서도 파일 업로드 시 이벤트 발행이 필요할 수 있어서,
이번에 추가된 DomainEventPublisher/DomainEventEnvelope 패턴 그대로 재사용할 예정입니다.

@hywznn
hywznn restored the agent/25-reliability-outbox branch July 27, 2026 00:48
@hywznn
hywznn deleted the agent/25-reliability-outbox branch August 7, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Reliability] 이벤트 유실 방지·재처리 기반 구현

3 participants