Sitemap
MUSINSA techblog — 무신사 테크 블로그

팀 무신사의 기술 이야기를 공유합니다. — 무신사 테크 블로그

“이번 달도 밤샘 정산입니다.” — 정산 시스템은 왜 필요했을까 (설계편)

10 min readJan 21, 2026

--

“이번 달도 밤샘 정산입니다.” 테크 블로그 시리즈

  1. 정산 시스템은 어떻게 만들었을까 (실전편)
  2. 정산 시스템은 왜 필요했을까 (설계편)
  3. 더 이상 밤샘하지 않아도 됩니다 (운영편)

들어가며

정산 업무를 경험해 본 조직이라면 익숙한 문장입니다.

정산은 매달 반드시 마감되어야 하지만, 그 과정은 늘 사람의 손과 기억에 크게 의존해 왔습니다. 데이터는 흩어져 있고, 기준은 상황마다 조금씩 달라지며, 한 번 계산한 결과도 다시 믿기 어려운 경우가 많습니다.

MASS는 이러한 문제의식에서 출발했습니다.

단순히 수기 작업을 자동화하는 것을 넘어, 정산이라는 행위를 시스템이 책임질 수 있도록 만들고자 했습니다.
이번 연재의 1편에서는 MASS를 설계하며 가장 먼저 고민했던 질문들, 그리고 그에 대한 설계 원칙을 공유합니다.

MASS란 무엇인가

MASS는 Musinsa Accounting & Settlement System의 약자로,
물류/운영 과정에서 발생하는 비용을 기준으로 파트너 업체와의 물류비 정산을 자동화하는 시스템입니다.

MASS는 다음 역할을 수행합니다.

  • 물류 운영 시 발생하는 입고/출고/반품/재고에 대한 원천 데이터를 수집
  • 조건과 단가를 기준으로 정산 금액을 계산
  • 일/월 단위로 정산을 집계하고 마감
  • 내부 담당자와 외부 파트너 업체가 동일한 기준의 정산 결과를 확인할 수 있도록 제공

즉, MASS는 단순히 정산 금액을 계산하는 도구가 아니라, 정산 결과에 대한 논쟁이 생겼을 때 최종 기준이 되는 시스템을 목표로 설계되었습니다.

Press enter or click to view image in full size

정산 시스템이 어려운 이유

정산은 단순히 숫자를 더하는 문제가 아닙니다.

  • 회계 마감이라는 절대적인 데드라인
  • 단 한 건의 오류도 허용되지 않는 정합성 요구
  • 과거 기준으로 언제든 다시 계산할 수 있어야 하는 재현성

기존에는 여러 단계의 수기 작업과 엑셀 검증을 통해 이를 처리하고 있었고, 이 방식은 높은 업무 부담과 오류 가능성을 동시에 안고 있었습니다.

MASS는 이 문제를 “정산을 더 빨리 하자”가 아니라 “정산을 시스템이 책임지게 하자”는 관점에서 접근했습니다.

설계 원칙: 속도보다 신뢰성

MASS를 설계하면서 가장 먼저 합의한 원칙은 다음과 같습니다.

정산 시스템은 빠르게 계산하는 시스템이 아니라 실패해도 다시 계산할 수 있는 시스템이어야 합니다.

이를 위해 아래 원칙을 아키텍처 전반에 반영했습니다.

  • 정합성과 멱등성
  • 결정적 계산(재현성)
    (결정적 계산: 같은 원천 데이터와 계산 기준을 사용하면 언제 다시 계산해도 동일한 결과가 나오도록 설계된 계산)
  • 감사 가능성(추적성)
  • 배치 실패 복구 및 재시작성

멱등성을 전제로 한 이벤트 처리

정산에 사용되는 원천 데이터는 이벤트 형태로 유입됩니다.

이벤트 기반 시스템에서 중복 수신이나 재처리는 피할 수 없는 상황이기 때문에, MASS에서는 이를 전제 조건으로 두었습니다.

  • 이벤트 재시도(Retry)와 격리(DLT)를 분리해 처리
  • 트랜잭션 식별자를 기준으로 서비스 레벨에서 멱등 갱신
  • 동일 이벤트가 여러 번 처리되어도 결과는 항상 동일

이를 통해 장애 상황에서도 안전하게 재처리할 수 있는 기반을 마련했습니다.

Press enter or click to view image in full size

이벤트 재시도(Retry)와 격리(DLT)를 분리해 처리

이벤트 기반 정산 시스템에서는 일시적인 실패와 구조적인 실패를 구분해 다루는 것이 중요합니다.

MASS에서는 이를 위해 이벤트 재시도(Retry)와 격리(DLT, Dead Letter Topic)를 명확히 분리해 처리합니다.

일시적인 네트워크 오류나 외부 의존성 문제로 인한 실패는 재시도를 통해 정상 흐름으로 복귀시키고, 반복 재시도 이후에도 처리할 수 없는 이벤트는 정상 파이프라인에서 분리해 DLT로 격리합니다.

트랜잭션 식별자를 기준으로 한 서비스 레벨 멱등 갱신

MASS에서는 모든 원천 이벤트에 대해 업스트림 시스템에서 이미 존재하는 고유 식별자를 활용합니다.
입·출고, 재고 스냅샷 이벤트에 포함된 식별자를 조합해 정산 도메인 관점의 트랜잭션 식별자로 사용합니다.

Get 박성민(Seongmin)’s stories in your inbox

Join Medium for free to get updates from this writer.

이 식별자는 단순한 DB Unique Key가 아니라, 정산 관점에서 이미 처리된 이벤트인지 판단하는 기준입니다.

  • 동일 식별자가 처음 들어오면 신규 정산 데이터로 처리
  • 이미 처리된 이벤트라면 결과를 유지하거나(no-op)
  • 정책 변경 등 의도적인 경우에만 갱신

이를 통해 MASS는 DB 예외에 의존하지 않고, 도메인 규칙에 기반한 멱등성을 확보했습니다.

결정적 계산을 위한 정산 로직

정산 금액 계산은 가능한 한 단순하고 결정적으로 설계했습니다.

  • 계산기 모듈은 입력값에만 의존
  • 금액 스케일과 반올림 정책을 고정
  • 계산 단계를 명확히 분리해 추론 가능성 확보

이 구조의 핵심은 “같은 입력이면 언제 계산해도 같은 결과가 나온다”는 점입니다.
덕분에 이의 제기, 기준 변경, 재처리 상황에서도 동일한 기준으로 다시 계산할 수 있습니다.

Press enter or click to view image in full size

계산기 모듈은 입력값에만 의존

정산 계산 로직은 외부 상태나 실행 시점에 따라 결과가 달라지지 않도록, 입력값만으로 결과가 결정되는 순수 함수 형태로 설계했습니다.

정산 계산에 필요한 모든 정보는 CalculateCommand와 그 안의 SettlementContext에 명시적으로 포함됩니다.

data class SettlementContext(
val settlementDate: LocalDate,
val settlementBaseType: SettlementBaseType,
)

SettlementContext는 “언제, 어떤 기준으로 정산하는가”를 명확히 표현하는 최소 단위의 컨텍스트로, 정산 계산 과정에서 현재 시각, 실행 환경, 전역 상태와 같은 외부 요인을 참조하지 않도록 만든 장치입니다.

이를 단순화하면 일 정산 계산 로직은 다음과 같은 형태를 가집니다.

# pseudo code

function calculateDailySettlement(command):
# command includes:
# - settlementContext (settlementDate, settlementBaseType)
# - serviceType, partnerId, brandId
# - logisticsCategoryCode, quantity, policy flags

# 1) 정산 기준일 기준으로 단가 조회
base = findBasePrice(command)
unitPrice = base.baseUnitPrice

# 2) 정산 기준일 기준으로 할인 정책 적용
discountedUnitPrice = applyDiscount(
unitPrice,
base.serviceChargeBase,
command.settlementContext.settlementDate,
command.partnerId,
command.brandId
)

# 3) 수량 반영
originAmount = unitPrice * command.quantity
discountedAmount = discountedUnitPrice * command.quantity

# 4) 정산 결과 반환
return {
unitPrice,
discountedUnitPrice,
originAmount,
discountedAmount,
remoteAreaUnitPrice (optional)
}

이 구조에서 계산 결과는 오직 다음 입력에 의해서만 결정됩니다.

  • 정산 기준일(settlementDate)
  • 정산 기준 유형(settlementBaseType)
  • 정책 정보(단가, 할인 조건)
  • 원천 데이터(수량 등)

덕분에 동일한 정산 컨텍스트와 입력이 주어지면 언제, 몇 번을 실행하더라도 동일한 정산 결과가 보장됩니다.

이는 이의 제기 대응, 기준 변경 이후의 재계산, 장애 복구 후 재처리 상황에서도 과거 정산을 동일한 기준으로 다시 계산할 수 있는 재현성을 확보하는 핵심 전제였습니다.

금액 스케일과 반올림 정책을 고정

정산 도메인에서 반올림은 “표현 방식”이 아니라 결과 자체를 바꾸는 규칙입니다.
특히 할인/수수료처럼 소수점이 개입되는 계산은 반올림 시점과 방식이 조금만 달라도 최종 금액이 달라질 수 있습니다.
일 단위로는 몇 원 수준의 차이처럼 보여도, 월 단위 집계로 누적되면 정산 금액 불일치로 이어질 수 있습니다.

그래서 MASS에서는 다음을 원칙으로 두었습니다.

  • 모든 금액 계산은 BigDecimal로 수행해 부동소수점 오차를 원천 차단
  • 할인 적용 시 스케일(MONEY_SCALE)을 강제로 고정
  • 반올림은 항상 동일한 정책(RoundingMode.DOWN)을 사용

예를 들어 비율 할인(RATE)의 경우, 할인율을 퍼센트에서 실제 rate로 변환한 뒤 곱셈을 수행하고, 그 결과를 반드시 동일한 스케일로 내림(DOWN) 처리해 할인 적용 단가를 결정합니다.

// RATE 할인: percent -> rate 변환 후 곱셈, 그리고 스케일/반올림 정책 강제
val percent = discount.discountValue.max(BigDecimal.ZERO).min(MAX_RATE_VALUE)
val rate = BigDecimal.ONE.subtract(percent.movePointLeft(RATE_SCALE))

val discountedUnitPrice =
unitPrice
.multiply(rate)
.setScale(MONEY_SCALE, RoundingMode.DOWN)
.max(BigDecimal.ZERO)

이처럼 “소수점이 생길 수 있는 지점”에서 정책을 코드로 강제해두면,

  • 계산 경로가 달라져도(일 정산/월 정산/재처리)
  • 실행 환경이 달라져도(다른 서비스/다른 배치)

항상 동일한 금액 산출이 가능해지고, 결과적으로 정산 결과의 재현성을 확보할 수 있습니다.

정산 시스템에서 반올림 정책은 구현 디테일이 아니라, “같은 입력이면 같은 결과가 나와야 한다”는 신뢰를 지키는 핵심 규칙입니다.

마치며

정산 시스템의 어려움은 계산식이 아니라, 다시 계산해야 하는 현실에 있습니다.
MASS는 멱등성과 결정적 계산이라는 설계 원칙을 통해 정산을 사람의 기억이 아닌 시스템의 책임으로 옮기고자 했습니다.

다음 글에서는 이러한 설계가 실제로 어떤 기술 선택과 구조로 구현되었는지, Kafka와 Spring Batch, Argo Workflow를 활용한 정산 시스템의 실전 이야기를 다룰 예정입니다.

Platform Business Operation 조직 및 팀 소개

무신사 플랫폼 비즈니스 오퍼레이션 조직은 국내외 물류 서비스, 재고 관리, 스토어 운영을 위한 물류 프로덕트를 구축하고 다양한 오프라인 비즈니스 모델에 맞춘 스토어 관리 시스템을 개발·고도화하고 있습니다.
또한, 무배당발 서비스를 포함한 무신사의 차별화된 고객 경험을 브랜딩하고 확장할 수 있는 멤버십 구조를 설계하며, 온·오프라인을 넘나드는 통합 커머스 경험을 기술로 실현하고 있습니다.

저희 팀은 OMS(주문관리시스템)를 기반으로 온라인 주문부터 재고·출고·배송·정산에 이르는 전 과정을 유기적으로 연결하고, 무신사의 다양한 오프라인 스토어를 효과적으로 운영할 수 있는 관리 시스템을 구축하여 고객이 온라인(무신사 스토어, 29CM 등)과 오프라인(무신사 스탠다드, 편집숍 등)에서 끊김 없는 쇼핑 경험을 누릴 수 있도록 지원합니다.

🚀 Platform Business Operation 한걸음 더 알아보기

🚀 팀 무신사 채용 페이지 (무신사/29CM 전체 포지션 확인이 가능해요)

🚀 팀 무신사 테크 소식을 받아보는 링크드인

🚀 팀 무신사 뉴스룸

--