자동화 slecs

충전 잔액 만료 배치를 원장 구조로 개선한 과정

목차

충전 만료 배치 만들면서 깨달은 것들

충전 잔액에 만료일을 붙이는 건 단순히 "유효기간 지나면 0으로 만들면 끝" 같지만, 실제로 짜보면 의외로 결정해야 할 게 많음.

만료를 "언제" 깎느냐 문제

처음엔 단순하게 만료일 지난 잔액을 매일 새벽 배치로 0 처리하려 했음. 근데 이렇게 하면 두 가지가 깨짐.

  • 사용자가 23:59:59에 결제 시도 → 동시에 배치 돌면 잔액 두 번 건드림
  • 만료 직전에 부분 사용 → 어떤 충전 건의 잔액이 얼마 남았는지 추적 안 되면 부분 만료가 안 됨

결국 충전 단위로 원장(ledger)을 쌓고, 사용 시점에 FIFO로 차감하는 구조로 갔음. 만료 배치는 "만료일 지난 충전 건의 남은 잔액만큼 만료 트랜잭션을 음수로 한 줄 박는다"가 전부. 잔액 컬럼을 직접 안 건드리니까 동시성 걱정도 줄고, 나중에 감사 들어와도 한 줄씩 다 보여줄 수 있음.

원장 조회 화면이 의외로 무거움

항목 처음 설계 바꾼 뒤
잔액 산출 매번 트랜잭션 SUM 충전 건별 잔액 컬럼 캐시
정렬 만료일 ASC 만료일 ASC + 충전일 ASC tiebreak
페이지 offset 페이징 커서(충전ID) 페이징

파트너 한 명당 충전 이력이 수만 건 쌓인 케이스가 있어서, 단순 SUM 으로 뽑으면 상세 페이지 들어갈 때마다 DB 가 비명 지름. 충전 건마다 remain_amount 컬럼을 두고 트랜잭션 발생 시 같이 갱신하는 방식으로 바꿈. 정합성은 야간 정합 배치가 다시 SUM 해서 비교.

쿠폰 사용내역 개선

쿠폰을 쓰면 어떤 충전 건에서 얼마가 빠졌는지 사용자한테 안 보여주던 게 컴플레인 1순위였음.

[2026-03-17 14:22] 쿠폰사용 -3,000원
  └ 2026-02-01 충전분 -1,500
  └ 2026-02-15 충전분 -1,500 (잔액 0, 만료처리됨)

이렇게 어떤 충전 건이 소진됐는지, 만료가 같이 일어났는지까지 한 줄에 묶어서 보여주니까 문의가 확 줄었음. 백엔드 입장에선 트랜잭션 한 건이 충전 N개를 건드릴 수 있다는 걸 모델에 반영해야 해서, 응답 DTO 를 1:N 으로 바꾸는 작업이 제일 귀찮았음.

정합성 배치는 따로 돌리는 게 정답

만료 배치랑 정합성 배치를 같은 잡에 묶었다가, 만료가 실패하면 정합성도 같이 죽어서 다음 날 새벽까지 알람이 한 번도 안 울리는 사고가 한 번 있었음. 두 개 분리하고, 정합성 배치는 결과 차이만 슬랙으로 쏘게 바꿈. 한쪽이 죽어도 다른 쪽이 진실을 알려주는 구조여야 함.

배운 것

  • 잔액은 컬럼이 아니라 원장이다. 컬럼은 캐시일 뿐
  • 배치는 하나의 책임만. 묶으면 장애 나면 같이 죽음
  • 사용자 화면에 "왜 이만큼 빠졌는지" 안 보이면 CS 가 다 떠안음

다음

댓글 0

첫 댓글 달아줘.