JSON 파일을 활용한 간이 Outbox 패턴으로 정산 데이터 지키기
이커머스나 B2B 플랫폼에서 '결제'만큼이나, 아니 어쩌면 그 이상으로 예민한 도메인이 바로 '정산' 입니다. 결제는 취소라도 할 수 있지만, 파트너사에게 이미 송금된 정산 금액을 되돌리는 것(비가역성)은 비즈니스적으로 엄청난 리스크를 동반하기 때문입니다.
화훼 도소매 거래 플랫폼 '하이패스'의 백엔드 시스템을 홀로 설계하면서, 저는 이 무시무시한 정산 자동화 파이프라인을 구축해야 했습니다. 이 글에서는 외부 지급대행 API를 호출할 때 발생할 수 있는 네트워크 장애나 공휴일 미처리 이슈를 방어하기 위해, 물리적인 JSON 파일을 활용한 간이 Outbox 패턴 을 어떻게 설계하고 적용했는지 공유합니다.
The Problem: 비가역적인 외부 API와 DB 부하
정산 처리는 보통 스케줄러를 통해 특정일(예: 5일, 20일) 자정에 일괄적으로 수행됩니다. 초기에 구상했던 흐름은 단순했습니다. 정산 대상 데이터를 DB에서 조회하고, 루프를 돌며 외부 지급대행 API를 호출한 뒤, 성공하면 DB의 상태 값을 '완료'로 바꾸는 것이었죠. 하지만 여기에는 치명적인 함정들이 숨어 있었습니다.
- 장애 발생 시 데이터 유실 및 중복 실행 위험: API 호출 중간에 네트워크 타임아웃이 발생하거나 서버가 다운된다면? 어떤 건이 처리되었고 어떤 건이 실패했는지 추적하기가 매우 까다로워집니다. 자칫 스케줄러가 재실행되어 이중 지급 이 발생할 수 있습니다.
- 반복적인 DB I/O 부하: 정산 건수가 수천, 수만 건으로 늘어났을 때 장애가 발생하여 재시도를 해야 한다면, 매번 무거운 정산 대상 쿼리를 DB에 다시 날려야 하는 부하 문제가 있었습니다.
- 공휴일 대응: 외부 지급대행사의 정책상 공휴일에는 송금이 실패합니다. 실패한 건들은 다음 영업일에 안전하게 이관되어 재시도되어야 했습니다.
The Solution: JSON 파일을 활용한 간이 Outbox 패턴
데이터베이스의 트랜잭션과 외부 API 호출이라는 두 개의 이질적인 작업을 안전하게 묶기 위해, MSA 환경에서 주로 쓰이는 Outbox 패턴 의 개념을 차용했습니다. 다만 한정된 리소스와 빠른 구현을 위해 별도의 Outbox 테이블이나 메시지 큐를 도입하는 대신, 물리적인 JSON 파일을 매개체로 활용하는 실용적인 아키텍처 를 설계했습니다.
설계된 정산 파이프라인 흐름:
- DB 상태 선점: 스케줄러(
node-schedule)가 실행되면, 우선 정산 대상 건들을 DB에서 조회한 후 상태를 '정산 대기'로 묶어 Lock을 겁니다. - JSON 파일 생성 (Outbox): 조회된 대규모 정산 대상 데이터를 메모리에 들고 있는 대신, 물리적인
settlement_target_YYYYMMDD.json파일로 서버 내부에 생성합니다. - 지급대행 API 호출: 생성된 JSON 파일을 읽어 들여 외부 지급 API를 호출합니다.
- 성공 시 파일 삭제: API 호출이 정상적으로 완료되고 DB 상태가 '정산 완료'로 업데이트되면, 비로소 해당 JSON 파일을 삭제합니다.
왜 하필 JSON 파일이었을까? 만약 정산 도중 DB 서버가 뻗거나 외부 API에서 공휴일 에러를 뱉어 로직이 중단되더라도, 처리해야 할 원본 데이터가 JSON 파일로 파일 시스템에 안전하게 보존 되어 있습니다. 다음 날 재시도 스케줄러가 돌 때 무거운 DB 조회를 다시 할 필요 없이, 남아있는 JSON 파일을 읽어 미처리된 내역만 API로 다시 쏴주면 됩니다. DB 부하를 획기적으로 줄이는 동시에, 최악의 DB 장애 상황에서도 금융 데이터를 안전하게 지켜내는 최후의 보루 역할을 한 것입니다.
(참고로 스케줄러 자체의 중복 실행을 막기 위해, PM2 단일 프로세스 환경에서 스케줄러가 철저히 격리되어 동작하도록 통제했습니다.)
The Result & Retrospective
결제/정산 불일치 0건의 달성 이전 프로젝트에서 겪었던 결제 데이터 불일치의 아픔을 완벽히 극복했습니다. JSON 파일 기반의 간이 Outbox 패턴과 철저한 트랜잭션 통제를 통해, 하이패스 플랫폼은 운영 기간 내내 단 한 건의 정산 누락이나 이중 지급 사고도 발생하지 않았습니다.
Next Step: 한계 인식과 정규 아키텍처로의 도약 현재의 방식은 트래픽이 적은 초기 스타트업 환경에서 개발 속도와 안정성을 모두 잡은 실용적인 '타협점'이었습니다. 하지만 이 경험을 바탕으로, 규모가 더 커진 시스템에서는 다음과 같이 아키텍처를 고도화할 계획입니다.
- DB 기반 정규 Outbox 패턴과 MQ 연동: 파일 시스템 의존성을 제거하고, 메인 트랜잭션과 동일한 DB 내에 Outbox 테이블을 두어 정합성을 100% 보장할 것입니다. 이후 이벤트 발행을 별도의 Message Queue(RabbitMQ, Kafka)로 위임하여 재시도 로직을 메인 서버와 물리적으로 디커플링하고 싶습니다.
- 멱등성 설계: 외부 API 호출 시
Idempotency-Key를 헤더에 포함하고, DB에 Unique Constraint를 걸어 네트워크 지연으로 인한 재시도 시에도 절대 이중 결제/정산이 일어나지 않도록 방어 로직을 겹겹이 쌓을 것입니다.
현실적인 제약 속에서 최선의 해결책을 찾고, 그 한계를 명확히 인지하여 다음 스텝의 정규 아키텍처를 그릴 수 있게 된 갚진 경험이었습니다.