Skip to content

feat(retention): DB-per-service retention/cleanup 스케줄러 (구현 ② PR3)#72

Merged
Kimgyuilli merged 5 commits into
mainfrom
feat/impl2-pr3-retention-scheduler
Jul 4, 2026
Merged

feat(retention): DB-per-service retention/cleanup 스케줄러 (구현 ② PR3)#72
Kimgyuilli merged 5 commits into
mainfrom
feat/impl2-pr3-retention-scheduler

Conversation

@Kimgyuilli

@Kimgyuilli Kimgyuilli commented Jul 4, 2026

Copy link
Copy Markdown
Owner

한눈에

각 서비스가 이벤트를 주고받으면서 쌓아두는 두 종류의 이력 테이블이 계속 커지기만 하고 지워지지 않던 문제를 닫습니다.

  • processed_events — "이 이벤트 이미 처리했어" 라고 기록해두는 중복 방지(멱등성) 장부
  • outbox_events — "이 이벤트 카프카로 보냈어" 라고 기록해두는 발행 이력

둘 다 시간이 지나면 오래된 행은 쓸모가 없어지는데, 지금까지는 지우는 로직이 없어서 무한히 쌓였습니다. 이번 PR이 오래된 행을 주기적으로 자동 정리하는 스케줄러를 붙입니다. (구현 ② "서비스별 DB 분리"의 마지막 PR3, ADR-0012 §D5)


왜 필요한가

processed_events 는 함부로 지우면 안 됩니다. 너무 일찍 지우면 "이미 처리한 이벤트"인데 장부에 기록이 없어서 같은 이벤트를 두 번 처리해버릴 수 있기 때문입니다.

그래서 "얼마나 오래 보관해야 안전한가"에 하한선(floor)이 있습니다. ADR-0012 D5는 이 하한선을 아래 4가지 중 가장 큰 값 이상으로 잡으라고 정합니다.

창(window) 의미
Kafka 토픽 보관 기간 카프카가 메시지를 얼마나 들고 있나
최대 consumer 다운타임 소비 서비스가 최대 얼마나 멈췄다 살아날 수 있나
DLQ 재처리 창 실패 큐를 수동으로 다시 밀어넣을 수 있는 기간
backfill/replay 창 과거 이벤트를 다시 흘려보낼 수 있는 기간

보관 기간을 이 하한선보다 짧게 설정하면 애초에 서버가 뜨지 않도록(fail-fast) 막았습니다. 잘못된 설정이 조용히 배포돼서 중복 처리 사고로 이어지는 걸 원천 차단하는 게 목적입니다.


무엇을 했나

  1. processed_events 정리 스케줄러 — 보관 기간이 지난 행을 주기적으로 삭제합니다.
  2. outbox_events 정리 스케줄러 — 이미 발행 완료(PUBLISHED)된 지 오래된 행만 삭제합니다.
  3. 잘못된 보관 기간이면 부팅 실패 — 위의 하한선 미만이면 서버가 뜨지 않습니다.
  4. 테스트 — 위 동작 전부 + 서비스별로 올바른 잡만 도는지 검증.

어떻게 작동하나

하한선 검사는 "설정값들을 서로 비교"하는 문제

단순히 "이 값이 null 이 아닌가" 같은 필드 하나짜리 검사가 아니라, 보관기간 ≥ max(4개 창) 이라는 여러 필드를 엮은 비교입니다. 그래서 common 모듈에 설정 클래스(IdempotencyRetentionProperties)를 하나 두고 @AssertTrue 로 이 교차 비교를 검증합니다.

이 설정은 cleanup 잡을 실제로 가진 서비스에서만 켜지도록(@EnableConfigurationProperties) 해서, 이 잡이 필요 없는 서비스(user)에는 새어 들어가지 않습니다.

삭제는 "한 번에 다 지우지 않고" 잘게 나눠서

오래된 행이 수백만 개 쌓여 있을 때 DELETE 한 방으로 지우면 테이블이 오래 잠기고 트랜잭션이 길어져 다른 작업에 영향을 줍니다. 그래서:

  • 기준 시각(cutoff)을 실행 시작 때 한 번만 계산하고,
  • 작은 묶음(batch)으로 나눠 반복 삭제합니다(batch-size × max-batches-per-run).
  • 각 묶음은 독립 트랜잭션이라 락을 오래 잡지 않습니다.
  • 삭제 기준 컬럼에 인덱스도 추가했습니다(V2 마이그레이션).

outbox 는 "보낸 것만" 지운다 (유실 방지)

정리 조건은 딱 상태 = PUBLISHED 이고 발행시각이 기준보다 오래됨 뿐입니다. 그 결과:

  • 아직 안 보낸 것(PENDING) → 보존
  • 보내다 실패한 것(FAILED) → 보존
  • 발행시각이 비어 있는 것 → 보존

아직 보내야 할 이벤트가 실수로 지워지는 일은 없습니다.

서비스마다 "필요한 잡만" 돈다

정리 잡 클래스를 그 잡이 필요한 서비스 모듈에만 물리적으로 배치했습니다(기존 코드의 복제 패턴 그대로). 그래서 잘못된 잡이 엉뚱한 서비스에 뜨는 오배선이 구조적으로 불가능합니다.

서비스 processed 정리 outbox 정리 비고
product 발행 서비스
order 발행 서비스
payment 발행 서비스
notification 소비 전용 (outbox 없음)
user 이벤트 이력 테이블 없음

notification 관련 추가 작업: notification 은 원래 소비만 해서 스케줄러 인프라(ShedLock)가 없었습니다. 이번에 정리 잡이 생기면서 ShedLock 의존성·설정·shedlock 테이블을 새로 추가했습니다.


설정은 전부 base 파일에

보관 기간·하한선·배치 크기 같은 동작 정책은 환경마다 달라지면 안 되므로 전부 application.yml(base)에 뒀고, 환경별 프로파일(-local/-k8s)에는 두지 않았습니다. (ADR-0007)

기본값: 보관 7일 / 하한선 max(7일·24시간·7일·7일)=7일 → 7일 ≥ 7일 통과.


Test plan

  • 단위 — 하한선 교차 검증·max 계산·잘못된 설정 시 부팅 실패(ApplicationContextRunner)·정책키가 프로파일에 새지 않는지 정적 검사
  • 통합(Testcontainers) — 만료 행 다중 batch 삭제·미만료/PENDING/FAILED/발행시각 없는 행 보존·서비스별 올바른 잡만 존재(product/order/payment=둘 다, notification=processed만, user=없음)
  • ./gradlew build test 8모듈 BUILD SUCCESSFUL

관련

  • Task: docs/TASKS.md — 구현 ② 서비스별 DB 분리 (PR3, L-008/011 종결)
  • Plan: docs/plans/task-impl2-db-per-service.md
  • ADR: ADR-0012 §D5 (retention floor), ADR-0007 (동작 정책은 base)

…② PR3)

processed_events/outbox_events 만료 행을 ShedLock 배치 스케줄러로 정리하고,
보존기간이 D5 floor(4창 max) 미만이면 부팅 실패(fail-fast)한다 (ADR-0012 D5).

- common: IdempotencyRetentionProperties(@AssertTrue cross-field)·OutboxRetentionProperties
  — 소유 서비스만 @EnableConfigurationProperties 활성(user 누출 없음)
- 스케줄러 소유 매트릭스(물리 배치): processed=product/order/payment/notification,
  outbox=product/order/payment, user=0
- 배치 삭제: cutoff 1회 계산 + batch-size×max-batches-per-run 반복, per-batch @transactional
- outbox predicate: status=PUBLISHED AND published_at<cutoff (PENDING/FAILED/NULL 보존)
- notification 신규 ShedLock 인프라(dep·ShedLockConfig·@EnableScheduling·shedlock 테이블 V2)
- 정책키 base-only(app.idempotency.*·app.outbox.retention) — ADR-0007
- V2: 삭제 기준 컬럼 인덱스
- floor cross-field 검증·max 계산(단위) + fail-fast(ApplicationContextRunner)
- base-only 정책키 배치 가드(RetentionPolicyPlacementTest, ADR-0007)
- processed/outbox 다중 batch 삭제 + PENDING/FAILED/published_at NULL/미만료 보존(통합)
- 서비스×잡 매트릭스: product/order/payment=both, notification=processed only, user=0
- PR3(P16~P19) 서비스×잡 매트릭스·floor cross-field·배치 삭제·base-only·replay 트레이드오프
  (plan-review 2 loop: 5→3건 수렴)
- work diff-review audit(GW-2 3 P2 반영)
- TASKS ② 서비스별 DB 분리 🔄→✅ (PR3 #72, L-008/011 종결) + 구현 focus ③ 이동
- PHASE4 PR3 이력(retention/cleanup·floor fail-fast·매트릭스·notification ShedLock)
@Kimgyuilli
Kimgyuilli merged commit 560adaa into main Jul 4, 2026
7 checks passed
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.

1 participant