← 메인 포트폴리오
Backend Portfolio · Settlement Domain

정산 시스템 설계·고도화

결제 시점에 단일 테이블로 정산금을 확정하던 레거시(payout)를 결제 시점과 정산 시점을 분리한 2단계 파이프라인(settlement_prep → SQS → settlement)으로 재설계했습니다. 혼합결제 환불을 결제수단별로 정확히 역산하는 오케스트레이션도 함께 구축했습니다. 나아가 정산을 받아 계산·평탄화하는 정산 서버(전략 엔진 · CQRS 조회 · 복구 파이프라인)까지 맡아 정산 데이터 생성부터 최종 정산·조회 모델까지 전체 파이프라인을 설계·구현했습니다.

기간 2024.02 – 운영 중 (~2년+) 역할 정산 도메인 단독 설계·구현·운영 스택 NestJS · PostgreSQL · AWS SQS(FIFO) · TypeORM · Multi-DB

진화 타임라인

2024.02 · Phase 0
payout 도입
결제 확정 시점에 단일 테이블로 total = 상품금액 + 배송비 − 수수료 정산 예정액 확정
2024.04 – 2025 · 한계 노출
멤버십·구독·다건·환불 증가 → 단일 금액 모델이 버그 온상화
수수료 계산 주석 처리, 정기결제 회차 누락, payouts[0] 단일 요소 가정 등 구조적 결함
2025.10 · Phase 1
settlement_prep 재설계
결제수단별 금액 분해 + 전략 패턴 + 순수 도메인 객체 도입 / 약 1,900줄 규모의 레거시 정산 엔진 재작성
2025.10 – 11 · Phase 2
AWS SQS(FIFO) 비동기 파이프라인
정산 계산을 결제 임계경로에서 분리, groupId·dedup 분리 설계로 순서·멱등 보장
2026.04 – 06 · Phase 3
order_payment + 환불 오케스트레이션
결제수단별 원장 정규화 + PostgreSQL advisory lock + 커밋 후 SQS 발행

아키텍처 — 2단계 비동기 정산 파이프라인

결제 완료
OrderPayment
멤버십 사용
UseLog
동기 · 즉시응답
SettlementPrep
결제수단별 금액 분해
(cash/point/pg)
DB commit 후
SQS FIFO
groupId=순서
dedupId=멱등
비동기 consume
정산 서버
익일 PG수수료 반영
→ Settlement 생성
settlement.processed.* 이벤트
조회 전용 DB
ReadDBUpdater 평탄화
CQRS · 비정규화

핵심: 메인 백엔드는 최종 settlement을 직접 만들지 않습니다. 결제 시점에 확정 가능한 정보만 prep에 기록하고 익일 확정되는 PG 수수료는 별도 정산 서버가 SQS로 받아 채웁니다 — 결제(동기)와 정산(비동기)의 시점 분리. 정산 서버는 Settlement를 만든 뒤 settlement.processed.* 이벤트로 조회용 Flat 테이블을 비동기 평탄화해 쓰기와 조회를 물리적으로 분리합니다(CQRS).

핵심 프로젝트

Phase 1
구조적 한계에 도달한 payout 진단 → settlement_prep 재설계
● 문제
  • 단일 금액 모델 vs 혼합결제 현실 — 결제수단별 금액 분해 개념이 없어, 혼합결제·부분환불이 들어오자 수수료 계산이 비활성화되고 확장 요구는 jsonb 필드에 누적 → 도메인 응집도 약화
  • 결제 시점 ≠ 정산 시점 — 정기결제 N회차에서 미리 생성된 payout이 회차 계산을 깨뜨림 → 시간 컬럼·휴리스틱 기반 보완 로직이 점진적으로 누적
  • payouts[0] 단일 요소 가정 — 0개·복수일 때 정산 누락/예외 → 방어코드 반복 누적
● 행동
  • 결제수단별 금액을 1급 컬럼으로 분리cashAmount / pointAmount / pgAmount / manualAmount / paymentFee
  • prep(준비) / settlement(최종) 2단계 분리 — 결제 시점에 확정 가능한 정보만 prep에 기록하고 익일 정보는 정산 서버가 채움
  • 순수 도메인 객체 SettlementPrepBuilder 추출 — 포인트→캐시→PG 순차 비례 배분 + 마지막 항목에 반올림 잔차를 몰아 합계 정확히 일치 (회계 정합)
  • 전략 + 팩토리 패턴 — Ticket(건당) vs Pass(정액 구독, 500건 청킹) 정산 분기를 추상화 → 유형 추가 시 서비스 수정 없이 확장
● 결과
  • 절차적으로 얽혀 있던 정산 로직을 전략별로 분리된 테스트 가능한 구조로 재설계, 레거시 정산 엔진 약 1,900줄 재작성
  • 8종 주문유형(EBOOK/ALADIN/POD/CASH/PACKAGE/SEPARATE/UPGRADE/멤버십)을 단일 출력 포맷으로 수렴
도메인 모델링전략/팩토리 패턴순수 도메인 객체레거시 마이그레이션관심사 분리
Phase 2
동기 정산 → AWS SQS(FIFO) 비동기 파이프라인
● 문제
  • 정산 계산(비율 조회·원저작자·분할정산·세금 계산)이 결제 요청 임계경로에서 동기 수행 → 응답 지연
  • 정산 실패가 주문 실패로 전파, 멤버십 대량 사용 시 부하 집중
● 행동
  • 무거운 계산을 SQS 뒤로 이동 — Service 의존성 5개 → 1개, 주문은 prep만 저장하고 즉시 응답
  • FIFO 두 키 분리 설계groupId(순서: 주문 직렬·청크 병렬) / deduplicationId(멱등: 5분 윈도우 중복 차단)
  • 멱등성 2중 방어선 — FIFO dedup(단기) + SqsMessageLog(영구 감사, 메시지 1건 → 비즈니스 엔티티 1:N 전개로 역추적)
  • 재시도 전략 — SDK 지수 백오프(상한 20s) → Slack 에스컬레이션 → 배치/수동 재처리
  • Producer 분리 — 일반 정산(고빈도) vs 환불/관리자(저빈도)를 별도 큐로 트래픽·멱등 전략 차등화
● 결과
  • 정산 계산이 결제 응답시간에서 제거, 정산 장애가 주문에 전파되지 않도록 격리
  • 순서가 필요한 단위(주문)는 직렬화하고 처리량이 필요한 단위(청크)는 병렬화하도록 groupId 단위를 설계
분산 시스템SQS FIFO멱등성부하 분산장애 격리
Phase 3
order_payment 도입 + 환불 오케스트레이션
● 문제
  • 한 주문을 포인트+캐시+PG 혼합결제 가능하나, 기존 order는 총액만 표현 → 부분환불 시 무엇을 얼마나 되돌릴지 계산 근거 없음
  • 동시 환불 요청(더블클릭·재시도) 시 PG 이중취소·자산 이중복구·정산 중복 생성 위험
● 행동
  • order_payment — 결제수단별 원장 정규화 — 환불은 음수가 아니라 status만 바꾼 mirror row + parent_payment_id(self-FK)
  • 2단계 분배 로직 — ① 주문 레벨: 포인트→캐시→PG 순 차감(내부자산 우선) ② 아이템 레벨: 비례 분배 + 잔차 흡수로 구매·환불 prep 합계 일치
  • PostgreSQL advisory lockpg_advisory_xact_lock('refund:'+orderId)로 다중 테이블 환불을 단일 키로 직렬화. 트랜잭션 스코프로 자동 해제
  • 커밋 후 SQS 발행runOnTransactionCommit으로 발행을 커밋 이후로 지연 → consumer가 미커밋 prep을 못 읽는 read-after-write race 차단
  • 보상 불가 구간 분리 — 보상 트랜잭션으로 되돌릴 수 없는 PG 취소 실패는 [REFUND_PG_CANCEL_FAILED] CRITICAL 로그로 별도 관리
● 결과
  • 혼합결제의 부분/전액·다회 환불을 결제수단별로 정확히 역산, 누적 환불의 원결제 초과 방지
  • 동시 환불을 직렬화해 PG 이중취소·자산 이중복구를 구조적으로 방지
동시성 제어advisory lock분산 트랜잭션보상 트랜잭션결제수단별 원장race condition 해결

정산 서버 — 정산 엔진 · 조회 최적화

메인 백엔드가 보낸 settlement_prep을 받아 최종 정산을 계산하고, 조회·리포팅을 읽기 전용 모델로 분리한 정산 전용 서비스. 코드 지표는 리포지토리 실측값 — 프로덕션 약 25k LOC(208 TS 파일), 테스트 비중 약 37%, 정산 전략 9종, 엔티티·모듈 43/16 규모.

Engine
9개 정산 모델을 Strategy + Template Method로 통합
● 설계
  • 단건·이용권·월정액·사용량 구독·문항별 정산(영어, 국어)·PG수수료 동기화 등 모델마다 데이터 출처와 분배 수식이 다르지만, execute() → getTargets() → processTargets() → buildResult() 골격은 동일
  • 공통 골격을 BaseSettlementStrategy의 템플릿 메서드로 고정 → 신규 정산 모델은 공통 실행 흐름을 재사용하고 모델별 계산 규칙만 구현하도록 분리해, 기존 전략 수정 없이 새 모델을 추가
  • 계산은 3계층 파이프라인 — 기본 분배 → 정산 타입별(이용권 COUNT/AMOUNT, 문항별 정산 3단계 단가·상한 80원, 일부 저자 유형은 정액 10원) → 저자/출판사 계약 비율 → 최소금액 보정(최소 10원)
● 효과
  • 유형 증가에 따라 커지는 조건 분기를 전략 패턴으로 구조화. 전략별로 분리·테스트 가능한 구조
Strategy/Template Method계산 규칙 분리정산 데이터 모델링
Correctness
금액 정밀성과 회계 정합성 — 정수 연산·불변성·감사 추적
● 설계
  • 부가세 round(price / 11), 분배가 round(price / N) 정수 연산으로 계산하고 잔여 손실을 통제
  • 핵심 원칙은 확정된 정산 금액을 in-place로 수정하지 않는다는 것입니다 — 변경이 필요하면 기존 건을 취소(soft-delete)하고 새 건으로 재정산합니다
● 근거
  • 회계·세무·저작권 정산은 "언제·무엇이·왜 바뀌었는가"가 데이터에 영원히 남아야 분쟁과 마감 정정에 대응할 수 있고 in-place update는 그 과거를 지웁니다
정수 연산 분배불변성감사 추적(audit trail)
CQRS
멀티 DB 분리와 이벤트 기반 비정규화
● 설계
  • 트랜잭션용 정규화 DB(master/slave 복제)와 조회·리포팅용 비정규화 DB를 물리적으로 분리
  • 정산 전략이 처리를 끝내면 settlement.processed.* 이벤트를 발행하고, ReadDBUpdater가 복잡한 JOIN 결과를 Flat 테이블로 평탄화
  • 조회용 평탄화 실패가 정산 본처리에 영향을 주지 않도록 읽기 모델 갱신 오류는 별도로 기록하고 진행 — 장애 격리
● 효과
  • 대시보드·엑셀의 무거운 조회가 정산 쓰기 트랜잭션을 방해하지 않음. 천만 건대 조회를 다중 조인 없이 단일 테이블 스캔으로 처리 (메인 포트폴리오 ② 조회 개선과 이어지는 구조)
CQRS읽기/쓰기 분리이벤트 기반 비정규화장애 격리
Recovery
오류 분류·추적·복구 파이프라인 — 검증 3단계 · 에러코드 분류 · 복구 액션 표준화
● 설계
  • 정산 오류를 "나면 사람이 본다"가 아니라 분류 → 추적 → 표준화된 복구 파이프라인으로 전환
  • 생명주기별 검증 3단계 — Creation/입력 검증(출처 ERR_03/04) → Strategy/계산 검증(금액 정합 ERR_12/14) → Scheduler/배치 검증(합계·중복 ERR_13/90)
  • 에러코드를 유형별로 분류하고 복구 액션을 표준화 — CONTRACT_REMAP(계약변경 재계산) · REGENERATE(완전 재생성) · CREATE_MISSING(누락 보충) · FULL_AUDIT(월별 9종 병렬 점검) · REFUND/RECALL(관리자 액션)
  • ErrorLog(감지) + ResolutionLog(해결 이력) 2계층 로깅으로 누가·어떻게·무엇을 고쳤는지 영구 기록
복구 파이프라인에러코드 체계2계층 로깅운영 안정화
Idempotency
멱등성과 대용량 — 재시도가 데이터를 망치지 않게
● 설계
  • 생성 중복 차단 — prepId/payoutId 기준 기존 정산 조회 후 있으면 그대로 반환
  • UPSERT 에러로그orUpdate + indexPredicate('resolved_at IS NULL')로 같은 오류 N회 감지돼도 행 1개, 해결된 건은 재오픈 안 됨
  • soft-delete 재생성 — 재생성 시 원본 createdAt 유지, 삭제 건은 중복검사에서 자동 제외
  • 스트리밍 엑셀pg-query-stream → 백프레셔 → ExcelJS WorkbookWriter → S3, 시트당 100만 행 자동 분할로 수백만 행을 상수 메모리로 처리
멱등성UPSERT 부분 인덱스soft-delete스트리밍

핵심 결과

엔지니어링 결과는 비즈니스 지표로도 이어졌습니다. 파트너 계약은 2026.06 기준 약 500건 규모로 확대됐고 대표 거래 구조 산식 기준 단순영업이익률은 -4%에서 +16%로 개선됐습니다(실제 회계 실적이 아닌 산식 시뮬레이션 기준). 정산 마감·보정 업무는 개발팀 의존에서 담당자 셀프서비스로 전환됐습니다.