Files
rtgs/docs/RTGS 삼중화(B1-B3) 설계.md
rtgs 58ca23b5d9 Initialize RTGS prototype repository
3센터 A-A-A RTGS 실시간총액결제 프로토타입 최초 버전관리 시작.
- backend: Kotlin/Gradle 멀티모듈(sequencer, common, 채널/센터 모듈)
- frontend: Vite + TS
- infra: docker-compose, prometheus/grafana, ELK 네이티브 스크립트
- loadtest(k6), sim(장애/순서/정합성 시나리오), docs(PoC 보고서/복원력방안)
2026-07-15 15:37:32 +09:00

12 KiB

RTGS 3센터 삼중화 설계 (B1·B2·B3) — 구현 착수 확정본

항목 내용
목적 2센터 PoC → 3센터 Active-Active-Active 전환. 코드 구현은 현 PC, 전체 3센터 실측은 고사양 PC 이관 후
범위 B1 순번기 이중화·무손실, B2 정족수(2/3) 완결, B3 순서결번 처리 + 멀티센터 런타임·설정 공통화
전제 코드는 이미 CENTER_ID/CENTER_COUNT 파라미터화. §9 결정사항 확정 완료(단일 인스턴스 3DB·인메모리 ack·순번기 3인스턴스)
실행 환경 실측 전제 = 고사양 PC(40코어/128GB) 3터미널. 단, 현 개발 PC는 8코어 → 이 PC는 코드/빌드/단위테스트/경량 스모크(1~2센터)까지, 전체 3센터 21 JVM 실측은 이관 후
상태 설계 확정 + 코드 구현·빌드·단위테스트 완료(2026-07-13, 8코어 PC) · 3센터 실측은 고사양 PC
구현 산출물 B1/B2/B3 코드 + V4__seq_outbox.sql + 단위테스트(QuorumAggregator·GapBuffer) + run-dc2/dc3.cmd·init-3centers-db.cmd. 기동·검증은 docs/RTGS 3센터 이관·기동 체크리스트.md 참조

0. 핵심 원리 (재확인)

  • 결정론적 재생: 3센터가 같은 저널을 같은 순서로 재생하면 각자 동일 상태 도달 → 원장 데이터 동기화 불필요(양팀장님 이론 맞음).
  • 센터 간 조율이 필요한 건 딱 둘: (a) 하나의 전역 순서 생성(순번기) + (b) 완결 선언 시 과반 확인(정족수).
  • 재해 규율: 1센터 down = 서비스 지속(복구 후 밀린 저널 진도처리=따라잡기) / 2센터 down = 서비스 중단(안전정지) / 복구 후 3센터 정상 재개.

1. 현재 상태 (전환 대상)

구분 현재(1센터) 삼중화 전환
Sequencer 단일 인스턴스, seq=인메모리→DB max복원 durable-before-publish + 리더선출(3인스턴스) + 펜싱
완결(Prada) confirmations=1 하드코딩 → 즉시 ACCC 센터별 applied ack 집계 → 과반(2/3) 후 ACCC
순서(Hermes) gap 버퍼링(무한대기 가능) gap 타임아웃 시 durable(Kafka/journal_log)에서 결번 pull
원장 DB 단일 rtgs 센터별 rtgs_dc1/dc2/dc3
서비스 1세트(7) 3세트(센터별) + 단일 순번기 그룹
설정 서비스별 yml 중복 공통 yml + env 파라미터화(A안)

2. 멀티센터 런타임 구성 (단일 호스트, 3터미널)

flowchart TB
  subgraph SHARED["공유 인프라 (단일)"]
    K["Kafka :9092<br/>rtgs.inbound / rtgs.journal(단일파티션) / rtgs.result(=applied-ack) / rtgs.notify"]
    PG[("PostgreSQL :5433<br/>rtgs_dc1 · rtgs_dc2 · rtgs_dc3")]
    OBS["Prometheus:9090 · Grafana:3000 · ELK"]
  end
  subgraph SEQ["전역 순번기 (그룹=sequencer, 1 active=리더)"]
    S1["seq@DC1"]:::a --- S2["seq@DC2"]:::s --- S3["seq@DC3"]:::s
  end
  DC1["DC1 스택 809x<br/>Chanel/Dior/Hermes/Prada/Gucci/LV"]
  DC2["DC2 스택 819x<br/>동일"]
  DC3["DC3 스택 829x<br/>동일"]
  K --- SEQ
  K --- DC1 & DC2 & DC3
  DC1 --> PG
  DC2 --> PG
  DC3 --> PG
  classDef a fill:#cfe8cf; classDef s fill:#eee

포트 오프셋 (센터별 +100): DC1 80908099·5174 / DC2 81908199·5175 / DC3 8290~8299·5176. 공유: Kafka 9092 · PostgreSQL 5433(3 DB) · Prometheus 9090 · Grafana 3000 · ELK 9200/5000/5601. 컨슈머 그룹: 원장서비스는 센터별 접미사(예: hermes-DC1/DC2/DC3) → 각 센터가 저널 전량 독립 재생. 순번기는 단일 그룹 sequencer(3인스턴스, 1파티션 → 1개만 활성 = 리더).

구현지점(현 코드 상태): @KafkaListener(groupId="hermes") 등 groupId가 하드코딩되어 있음 (dior/hermes/prada/chanel/sequencer 각 서비스). 센터별 독립 재생을 위해 원장서비스 groupId를 CENTER_ID 접미사로 파라미터화 필요 → SpEL groupId = "hermes-#{@centerId}" 또는 groupId = "\${rtgs.group.hermes}"(프로퍼티 주입). 순번기만 예외(접미사 없이 단일 그룹 sequencer 유지).


3. B1 — 순번기 이중화 + 무손실 (durable-before-publish)

현 코드 상태(전환 대상): SequencerService.kt는 인메모리 AtomicLong + 부팅 시 @PostConstructMAX(journal_log.global_seq, transfer.global_seq) 복원. 순번기 자신은 journal_log/outbox를 쓰지 않고 바로 rtgs.journal 발행(발행-후 크래시 시 무손실 미보장). 따라서 아래 SEQUENCE·outbox·재발행은 전량 신설이다(현 코드에 없음).

리더선출(간결·견고): 순번기 3인스턴스를 같은 컨슈머 그룹 sequencerrtgs.inbound(단일 파티션) 구독 → Kafka가 파티션을 1개 인스턴스에만 배정 = 자동 단일 리더. 리더 死 시 Kafka 리밸런스로 자동 승계(별도 선출 로직 불필요).

durable-before-publish (무손실·무결번):

  1. 리더가 접수 수신 → Postgres SEQUENCE global_seq_seq.nextval(원자적·durable 순번)
  2. 저널 엔트리를 outbox 테이블에 저장(commit) → 그 후 rtgs.journal 발행
  3. 크래시로 발행 전 죽어도, 재기동/승계 리더가 outbox의 미발행분을 재발행(at-least-once)
  4. 소비측은 seq 기준 멱등(이미 있음) → 중복 무해

펜싱(split-brain): nextval이 원자적이라 중복 순번 불가. 순간적 이중리더가 있어도 서로 다른 seq만 발급 → 저널 오염 없음. (엄격 fencing이 필요하면 outbox에 leader epoch 컬럼 추가—후순위)

작업: 새 서비스/모듈 대신 SequencerService 개편 + global_seq_seq·journal_outbox + 재발행 스케줄러.

  • Flyway 위치: 마이그레이션은 backend/louisvuitton/src/main/resources/db/migration/에 있고 현재 V1__baseline·V2__admin_login·V3__service_heartbeat까지 존재 → SEQUENCE+outbox는 V4__seq_outbox.sql 로 추가.
  • 재발행: journal_outboxpublished 플래그 컬럼 → 발행 성공 시 true. 스케줄러가 미발행분 주기 재발행(at-least-once).

4. B2 — 정족수(2/3) 완결

흐름: 각 센터 Hermes가 seq 적용(선저널 commit) 후 applied ack 발행 → rtgs.applied(bmi, seq, center). 각 센터 Prada가 bmi별 서로 다른 center 수 집계≥ quorum(=CENTER_COUNT/2+1) 도달 시 ACCC 확정. 결과통보는 origin 센터만 → 과반 도달 후 pacs.002 송부.

구현 결정 — rtgs.result를 applied-ack으로 재활용(별도 토픽 불필요):

  • 당초 rtgs.applied 신설을 검토했으나, rtgs.result가 이미 커밋 후에만(publish-after-commit) 발행되고 processedCenter를 담으므로 "그 센터가 durable 적용했다"는 ack와 정확히 동일하다. → 토픽/Hermes 변경·마이그레이션 없이 더 단순·안전. Topics에 APPLIED 추가하지 않음.
  • 구현 완료(2026-07-13):
    • prada/QuorumAggregator.kt(신규, 순수로직+단위테스트 6): bmi별 서로 다른 processedCenter 집계, 과반 최초 도달 판정, 완결 후 멱등 가드.
    • PradaService.finalize(): confirmations=1 제거 → aggregator.record() 기반. 과반 미달=ACSP, 도달=ACCC(반려면 RJCT). @KafkaListener(groupId="prada-${rtgs.center-id}")로 전 센터분 소비.
    • 중복 통보 방지: origin 센터 Prada만 notify 발행(notice.originCenter == centerId). NotificationService의 origin 필터는 그대로 유지(이중 안전).

ack 집계 저장(§9.4 확정): 인메모리 맵(bmi→집계한 center 집합). finality_ack 테이블은 불채택(간단성 우선).

재기동 경계조건(인메모리 손실 대응):

  • 각 센터 Prada는 자기 컨슈머그룹으로 rtgs.applied를 소비 → 재기동 시 커밋된 오프셋에서 재개.
  • 인메모리 집계 맵이 재기동으로 비어도: 이미 ACCC 확정분은 transfer.status에 영속되어 안전(재판정해도 멱등). 아직 과반 미달(ACSP)인 건만 이후 도착하는 ack로 재집계 → 최종 수렴. 이중완결/누락 없음.

가용성 매핑:

  • 1센터 down → 남은 2센터가 ack 2개 → 과반 충족 → 완결 지속.
  • 2센터 down → ack 1개 → 과반 미달 → 완결 보류(안전정지). 복구 후 자동 재개.

5. B3 — 순서 결번(gap) 처리

  • B1으로 영구 결번 소멸(발급=durable). 지연으로 인한 일시 gap은 Hermes 버퍼가 재정렬(S4 검증됨).
  • 방어책: expected 순번이 T초 이상 미도착 시 → 경보 + Kafka에서 해당 seq 오프셋 재읽기(pull) (durable 저널에서 직접 가져옴). 스킵 금지(실제 엔트리는 반드시 적용).
  • 지표: rtgs.journal.gap(이미 추가) + 타임아웃 카운터 → Grafana 경보.

6. 재해·복구 시나리오 (검증 항목)

시나리오 기대 검증(7/13)
평상시 3센터 동일 순서·동일 잔액, 완결 즉시 과반 3 DB 대사 일치, 총액 보존
1센터 강제종료 서비스 지속(과반 2/3), 이중지급 0 나머지 2센터 완결 지속
재해센터 복구 밀린 저널 진도처리(따라잡기) 후 3센터 수렴 오프셋 재생→잔액 일치
2센터 종료 완결 보류(안전정지), 오처리 0 과반 미달로 ACCC 정지
2센터 복구 따라잡기 후 3센터 정상 재개 완결 재개·정합성
순서 역전/gap 재정렬·pull로 정답 수렴 S4 확장(3센터)

7. 설정 공통화 (A안, 삼중화 전제작업)

  • commonapplication-common.yml(datasource·kafka·management 공통) → 각 서비스 spring.config.import.
  • 센터·포트·DB·그룹접미사는 env 파라미터: CENTER_ID, CENTER_COUNT=3, POSTGRES_DB=rtgs_dc1, PORT_OFFSET, GROUP_SUFFIX.
  • 실행 스크립트 3종: run-dc1.cmd(현행) + run-dc2.cmd·run-dc3.cmd(env만 다름) + 공유 인프라/ELK/Prometheus는 1회 기동.

8. 착수 체크리스트 (순서)

A. 현 개발 PC(8코어)에서 가능 — 코드·빌드·경량 검증 2. 설정 공통화(A안) + env 파라미터화(CENTER_ID/CENTER_COUNT/PORT_OFFSET/GROUP_SUFFIX) + run-dc2/dc3.cmd. 3. DB 분리: 단일 인스턴스(5433)에 rtgs_dc1/dc2/dc3 생성(Flyway가 각 DB 스키마 생성). 4. B1: V4 SEQUENCE+outbox+재발행 스케줄러, 순번기 3인스턴스(그룹 리더선출) → 단일 순번 검증. 5. B2: rtgs.applied 토픽 + Hermes ack 발행 + Prada 과반 집계(인메모리) → 완결 규율. 6. B3: gap 타임아웃·pull. → 각 단계마다 **./gradlew build + 단위테스트 + 경량 스모크(관자 1~2센터 기동)**로 확인.

B. 고사양 PC 이관 후 — 전체 3센터 실측

  1. 환경 이관 검증: C:\ai-dev 복사본으로 빌드·기동 확인(포터블 무설치 철학이라 경로만 동일하면 OK).
  2. 재해/복구 시나리오(§6) + 성능 실측(터미널3, 21 JVM, k6/테스트화면, 순번·지연 병목).
  3. sim S3(정족수/펜싱)·S5(스파이크) 추가.

9. 결정사항 — 확정 완료 (2026-07-13, 양팀장님)

# 항목 확정 비고
1 PostgreSQL 단일 인스턴스 · 3 DB(5433, rtgs_dc1/dc2/dc3) 자원 절약, 8코어 PC 적합. Flyway가 각 DB 스키마 생성
2 프론트 콘솔 DC1 콘솔 하나로 3센터 조회(제안) 간단. 센터별 3콘솔(5174~5176)은 필요 시 후속
3 순번기 배치 3인스턴스(센터당 1, 그룹 sequencer 리더선출) Kafka 리밸런스 자동 승계
4 applied ack 저장 인메모리 맵 finality_ack 테이블 불채택. 재기동 경계조건은 §4 참조

10. 리스크·주의

  • 21개 서비스 JVM 동시 구동은 고사양 PC 전용(여유). **현 8코어 PC에선 경량 스모크(1~2센터, 일부 서비스)**로 코드 정상동작만 확인하고, 전체 동시기동 실측은 이관 후. 개별 명령 기동(bash for-loop 동시기동 실패 이슈).
  • Kafka 저널은 반드시 단일 파티션(전역 순서). 3센터가 같은 토픽 독립 그룹으로 소비.
  • 정합성 대사: 3 DB의 transfer/account가 동일해야(총액·상태·순번). 자동 대사 스크립트 준비.
  • 참조: 전문 보강(BAH/pacs.002 검증)은 docs/pacs.7z 기준(B7과 함께).

본 설계는 구현 착수 기준 확정본(리뷰·보완 2026-07-13). §9 결정 반영 완료 — 이후 구현 중 세부만 조정.