개발 slecs

채널 일괄 등록에 계층별 요율 검증 추가로 정산 오류 방지

목차

채널 계층 구조를 다루는 정산 시스템은, 겉보기엔 단순한 트리 구조지만 실제로는 꽤 까다롭다. 상위 채널이 하위 채널에게 일감을 넘기고, 그 과정에서 각 계층이 마진을 가져가는 구조인데, 문제는 이 요율 순서가 조금이라도 뒤집히는 순간 마진이 음수가 된다는 것이다. 이번 작업은 그 문제를 막기 위한 검증 로직을 일괄 등록 흐름에 끼워 넣은 것이었음.

왜 일괄 등록인가, 그리고 왜 지금

채널 포털은 외부 채널 사업자가 직접 들어와서 잔액, 거래 내역, 하위 채널 현황을 확인하는 공간이다. 지금까지는 채널 등록을 한 건씩 처리했는데, 특정 시기마다 신규 채널이 묶음으로 들어오는 패턴이 반복됐음. 한 건씩 처리하는 게 운영 부담이 되다 보니 엑셀 업로드로 다수 채널을 한 번에 등록하는 기능이 필요해진 것.

일괄 등록 자체는 어렵지 않다. 엑셀을 파싱하고 유효성 검사 후 DB에 밀어 넣으면 된다. 진짜 문제는 업로드 시점에 계층 관계를 어떻게 검증하느냐였음.

채널 계층 구조는 대략 이렇다:

계층 역할 요율 예시
루트 채널 최상위, 전체 마진 설정 3.0%
중간 채널 하위에 재위탁 4.5%
말단 채널 실제 거래 발생 6.0%

상위로 올라갈수록 요율이 낮아야 마진이 생긴다. 상위 3%, 하위 4.5%라면 중간 채널이 1.5%를 가져간다. 이게 뒤집히면 차액이 음수가 되고, 정산 배치 실행 시 실수령액 계산이 틀어진다.

단건 등록 때는 이 검증이 UI 레벨에서 어느 정도 걸렸는데, 일괄 업로드는 백엔드 검증만으로 막아야 해서 트랜잭션 안에서 계층 순서를 체크하는 로직을 추가했다.

def validate_tier_rates(channels: list[ChannelRow]) -> list[str]:
    errors = []
    channel_map = {ch.id: ch for ch in channels}

    for ch in channels:
        if ch.parent_id and ch.parent_id in channel_map:
            parent = channel_map[ch.parent_id]
            if parent.rate >= ch.rate:
                errors.append(
                    f"{ch.name}: 상위 채널({parent.name}) 요율 {parent.rate}%가 "
                    f"현재 채널 {ch.rate}%보다 높거나 같음"
                )
    return errors

상위 채널의 요율이 하위보다 높거나 같으면 에러 목록을 반환하고, 트랜잭션 전체를 롤백한다. 부분 성공은 허용하지 않았음. 어떤 채널은 들어가고 어떤 채널은 누락된 상태로 관계가 깨지는 게 더 위험하기 때문이다.

정산 계산에서 반올림이 갖는 의미

계층별 요율 검증과 함께 정산 화면 쪽도 손봤는데, 여기서 의외로 시간이 걸린 게 반올림 정책이었음.

수수료 계산에서 소수점 처리를 어느 방향으로 하느냐는 꽤 민감한 문제다. 원 단위로 맞출 때 0.5원 이상이면 올림, 그 이하면 내림이라는 단순한 규칙으로 하면 경우에 따라 채널이 낸 수수료 합산과 플랫폼이 실제로 받은 금액이 몇 원씩 차이 날 수 있다.

업계 관행상 수수료를 내는 쪽에 유리하게, 즉 수수료는 내림으로 적게 부과하고 실수령액도 내림으로 정확하게 지급하는 방식이 분쟁을 줄인다. 누가 봐도 플랫폼이 반올림으로 이득 보는 구조가 아니라는 게 명확해지기 때문이다.

import math

def calc_fee(amount: int, rate: float) -> int:
    # 수수료는 내림 - 채널에게 유리하게
    return math.floor(amount * rate / 100)

def calc_net(amount: int, fee: int) -> int:
    return amount - fee

정산 UI 쪽에서는 금액 표기 일관성도 맞췄다. 천 단위 구분자와 '원' 단위는 기본이고, 마이너스 금액이 발생했을 때 빨간색으로 구분하는 처리를 추가했음. 정산 화면에서 사용자가 가장 먼저 보는 건 결국 숫자고, 그 숫자가 눈에 바로 들어와야 실수가 줄어든다.

정산 배치 멱등성, 항상 챙겨야 하는 것

이번 작업을 하면서 다시 한번 챙긴 건 정산 배치의 멱등성이다. 같은 기간을 두 번 실행해도 결과가 같아야 한다는 원칙인데, 말은 쉬워도 구현을 소홀히 하면 재실행 시 중복 정산 레코드가 생기거나 금액이 두 배로 잡히는 버그가 난다.

방어 패턴은 단순하다. 정산 실행 전에 해당 기간 레코드가 이미 있는지 확인하고, 있으면 덮어쓰거나 스킵하는 방식. INSERT ... ON CONFLICT DO UPDATEupsert 패턴으로 처리하는 게 일반적이다. 정산 레코드에 (channel_id, period_start, period_end) 유니크 제약을 걸어두면 DB 레벨에서도 중복을 막을 수 있음.

이번 변경 자체가 크지 않았다는 건 사실이다. 파일 몇 개, 로직 한 덩어리. 하지만 이런 류의 검증이 빠진 상태로 운영됐다면 요율 순서가 뒤집힌 채로 채널이 등록되고, 정산 배치가 돌고, 나중에 부정확한 숫자를 보고 나서야 문제를 파악하게 됐을 것이다. 금전 관련 버그는 발생 시점이 아니라 발견 시점이 중요한데, 발견이 늦을수록 수습이 복잡해진다. 입력 단계에서 막는 게 제일 싸다.

댓글 0

첫 댓글 달아줘.