ESD 플랫폼

이벤트 기반 게임 스토어 백엔드 — 결제·지급 흐름의 정합성

GitHub 저장소 →
기간2026.08 — 진행 중
역할개인 프로젝트 · 백엔드 전체
규모서비스 9 + 게이트웨이 · 단일 저장소
기술Java 21 · Spring Boot 3.5 · Kafka (KRaft) · MySQL 8 · Redis · Elasticsearch · MongoDB · Prometheus · Tempo

Overview

게임 등록 요청과 심사부터 구매·결제·정산까지를 서비스 아홉과 게이트웨이로 나눠 한 저장소에서 개발했습니다. 서비스끼리는 직접 부르지 않고 Kafka 이벤트로 잇고, 같은 이벤트가 두 번 반영되지 않도록 Outbox / Inbox 패턴을 뒀습니다.

이 페이지에 적은 네 사례는 모두 문제 상황을 재현하는 테스트를 먼저 작성하고, 고친 동작을 그 테스트로 확인한 것입니다. 측정값은 장애 주입과 부하 테스트 회차의 실측이며, 조건은 각 사례에 적어 두었습니다.

이 페이지의 사례
  1. 정상 결제 자동 환불 — 환불 8 → 0건
  2. Outbox 발행 순서 역전 — 역전 0건 · ×1.13
  3. Kafka 구간 추적 단절 — p95 1.18s → 0.56s
  4. 삼켜진 결제 승인 — 승인 유실 0건

Engineering Cases

CASE 01

라이선스 지급 중 장애로 발생한 오환불

PROBLEM

부하를 건 상태에서 지급 서비스의 DB 접근을 60초 막자, 재시도 횟수를 모두 소진한 이벤트가 지급 실패로 판단되어 정상 결제 8건이 자동 환불됐습니다. 환불은 되돌릴 수 없고, 정산에서 보면 정상 처리로 보여 대사에도 걸리지 않습니다.

CAUSE

환불을 거는 근거가 “재시도를 다 썼다” 하나였습니다. 재시도 소진은 지급할 수 없다는 뜻이 아니라 지급됐는지 답을 못 받았다는 뜻이고, 여덟 건은 모두 60초 뒤면 지급될 결제였습니다.

SOLUTION

자동 환불 조건을 실제 지급 실패와 분리했습니다. 환불 직전에 저장소 장애인지와 이미 지급됐는지를 확인하고, 판단이 서지 않는 이벤트는 환불 대신 보류함으로 넘겨 원인을 고친 뒤 사람이 재투입하도록 바꿨습니다.

RESULT
8 → 0건
되돌릴 수 없는 환불
7건 · 1초
보류 후 재투입 → 전량 지급
800건 : 0
DB 차단 15분 회차 2회 · 환불 0건

같은 장애를 같은 조건으로 다시 주입해 확인했고, 회차마다 장애 없는 회차를 한 번 더 돌려 비교했습니다.

CASE 02

Outbox 발행 순서 역전 — 환불된 주문에 남는 라이선스

PROBLEM

한 주문의 결제 취소가 결제 완료를 추월하면, 회수할 라이선스가 없어 아무 일도 하지 않고 끝난 뒤 뒤늦게 도착한 결제 완료가 라이선스를 발급합니다. 돈은 돌려주고 게임은 그대로 남습니다.

CAUSE

적재 순서와 Kafka 전달 순서는 맞았고, 어긋나는 지점은 릴레이가 outbox에서 꺼내 Kafka로 넣는 한 군데였습니다. 발행에 실패한 이벤트를 건너뛰고 다음 건을 보내면서 같은 주문의 뒤가 먼저 나갔습니다.

SOLUTION

발행 배치를 주문번호 단위로 묶어, 같은 주문은 앞이 성공해야 다음을 보내고 서로 다른 주문은 한꺼번에 걸도록 바꿨습니다. 배치가 갈릴 때도 순서가 깨지는 것을 테스트로 발견해, 실패한 이벤트의 다음 시도 시각을 후행 이벤트에 전파해 둘을 같은 회차로 되돌렸습니다.

RESULT
역전 0건
재현 테스트 6건이 빌드마다 실행
×1.13
138 → 156.1 events/s
0.47 → 0.28초
배치 처리 시간 · 왕복 200회 → 대개 1회

부하 테스트에서 성능이 나빠진 회차가 있었지만, 릴레이를 띄우지 않은 회차도 같은 숫자를 내 원인이 측정 환경이었음을 확인하고 수정은 되돌리지 않았습니다.

CASE 03

Kafka 구간에서 끊기는 분산 추적

PROBLEM

주문 하나를 order → payment → license → settlement로 따라 읽으면 첫 서비스 다음부터 추적 식별자가 빈칸이었습니다. 추적 라이브러리를 붙이자 빈칸 대신 엉뚱한 값이 들어갔습니다.

CAUSE

Outbox는 지금 커밋하고 나중에 보낸다고 전제하고, 라이브러리는 보내는 스레드가 요청을 시작한 스레드라고 전제합니다. 각각은 맞지만 함께 쓰면 릴레이 폴링 회차의 값이 실립니다.

SOLUTION

W3C traceparent를 이벤트와 같은 트랜잭션으로 outbox 행에 저장하고, 발행할 때 헤더에 실어 보냈습니다. 이벤트 본문과 계약은 손대지 않았습니다.

RESULT
서비스 6곳
하나의 traceId로 연결
1.18s → 0.56s
지급 표시 p95 · 폴링 1000 → 200ms
+1.5%p
mysql CPU 7.3 → 8.8% · 폴링 5배의 대가

흐름을 하나로 잇자 세 컨슈머가 모두 +527ms 뒤에 시작하는 것이 드러났고, 폴링 주기가 만드는 대기임을 부하 테스트 2회차로 확인했습니다.

CASE 04

멱등키로 결제를 조회해 삼켜진 승인

PROBLEM

결제사가 보내온 멱등키가 다른 주문에 재사용되면, 그 키로 찾은 다른 주문의 결제가 나와 승인이 중복 콜백으로 판정되고 조용히 무시됩니다. 라이선스 미지급·주문 미확정·정산 누락이 함께 일어나면서 예외도 경고도 남지 않습니다.

CAUSE

우리가 유일성을 보증할 수 없는 외부 값 하나에 결제 조회, 중복 판정, 유니크 제약이 모두 걸려 있었습니다. 조회가 틀리는 순간 나머지 둘도 함께 틀립니다.

SOLUTION

멱등키가 맡던 일을 셋으로 나눴습니다. 조회는 주문번호, 중복 판정은 처리 이력, 동시 콜백 차단은 행 잠금으로 옮겼습니다.

RESULT
유실 0건
키가 겹쳐도 승인이 무시되지 않음
12건
실제 MySQL 기반 통합 테스트
4단계
금액 검증 게이트 · 음수 수량 불변식 포함

다음 프로젝트 — SNG · 수집형 RPG 게임 서버