개발 slecs

채널 계층 요율 검증으로 정산 마진 오류 방지

목차

파트너 채널 구조는 생각보다 금방 복잡해진다. 상위 채널이 하위 채널을 모집하고, 그 하위가 다시 하위를 두는 다단계 수수료 구조가 되면 요율 관리가 곧 정산 정확도와 직결된다. "채널 A가 10%, 채널 B가 8%면 마진이 2%"라는 단순 계산은 계층이 3단계, 4단계로 늘어나고 예외 요율이나 기간 한정 프로모션까지 얹히는 순간부터 흐려진다. 그 복잡성을 담당자가 엑셀로 직접 관리하는 구조는 이미 한계가 온 상태다.

이번 작업의 발단은 채널 포털에 일괄 등록 기능을 붙이는 거였다. 엑셀로 다수 채널을 한 번에 올릴 수 있게 하는 기능인데, 업로드할 때 계층 요율이 역전된 상태로 들어와도 그냥 저장되는 문제가 있었다. 저장 시점에는 아무 오류 없이 통과하고, 나중에 정산 배치가 돌아갈 때서야 마진이 음수로 찍히는 걸 발견하게 되는 구조였다. 발견이 너무 늦다.

계층 요율 검증: 왜 업로드 시점인가

요율 오류를 방어하는 위치는 여러 단계가 있다.

  • DB 제약 조건으로 INSERT 자체를 막기
  • 정산 배치 진입 전 선검증 단계 추가
  • 업로드 시점에 즉시 반환

세 가지 중 앞단일수록 복구 비용이 낮다. 정산 배치에서 잡으면 오류 발생 타이밍이 실제 거래가 수만 건 쌓인 이후다. 정산 결과를 되돌리거나 재처리하는 공수가 커지고, 채널 사업자 입장에서는 "이미 확정됐다"고 생각한 숫자가 흔들리는 상황이 된다. 금액 오류보다 신뢰 손상이 더 오래 남는다.

규칙은 단순하다. 플랫폼(A) → 대리점(B) → 하위 대리점(C) 구조라면 A의 수수료율 < B의 수수료율 < C의 수수료율이어야 각 단계에서 마진이 양수로 유지된다. 이 순서가 뒤집히면 B가 A한테 돈을 내야 하는 구조가 되는데, 정산 결과에 마이너스가 찍히고 나서야 담당자 티켓이 올라온다.

검증 로직 자체는 복잡하지 않다.

def validate_channel_rate_hierarchy(channel: Channel) -> None:
    parent = channel.parent
    if parent is None:
        return  # 최상위 채널은 비교 대상 없음

    if channel.rate <= parent.rate:
        raise ValidationError(
            f"하위 채널 요율({channel.rate}%)은 "
            f"상위 채널 요율({parent.rate}%)보다 높아야 합니다. "
            f"(채널 코드: {channel.code}, 상위: {parent.code})"
        )

엑셀 일괄 등록에서 까다로운 점은 행 순서가 계층 순서를 보장하지 않는다는 거다. 부모 채널이 300행에 있고 자식이 50행에 있는 경우도 흔하다. 그래서 시트 전체를 먼저 파싱해서 채널 코드 기준 맵을 만들고, 루트부터 DFS로 검증을 내려가는 방식으로 처리했다. 오류가 생기면 어느 행, 어떤 채널 코드에서 어느 상위 채널과 충돌했는지 메시지에 담아 반환했다. "요율 오류가 있습니다"만 던지면 1000행짜리 파일에서 담당자가 직접 찾아야 한다. 그 수고는 시스템이 해줘야 한다.

채널 포털 대시보드와 정산 수치 표시

채널 포털은 각 외부 채널 사업자가 자신의 현황을 확인하고 설정하는 공간이다. 대시보드에서 잔액, 거래 내역, 하위 채널 현황을 한눈에 볼 수 있어야 하고, 정산 내역과 하위 채널 신청 처리도 여기서 이루어진다.

사업자가 가장 자주 보는 건 결국 숫자다. 그 숫자가 직관적으로 안 읽히면 문의가 온다. 실무에서 적용하는 표기 기준을 정리하면 이렇다.

항목 표기 원칙
금액 천 단위 구분자, '원' 단위 명시
마이너스 금액 빨간색 텍스트, 숫자 앞 - 기호
수수료율 소수점 둘째 자리까지 고정 표시
정산 기간 YYYY-MM-DD ~ YYYY-MM-DD 형식
하위 채널 수 직접 하위와 전체 하위를 구분 표시

마이너스 금액 표기가 특히 중요한 이유는 실수령액이 음수인 순간, 사업자가 "내가 돈을 내야 하는 상황"임을 즉각 알아채야 하기 때문이다. 색상 구분 없이 숫자만 있으면 화면을 훑다가 지나치기 쉽다. 이런 UI 세부 사항이 실제로 "정산이 이상하게 나왔다"는 문의 수를 줄인다.

수수료 반올림 방향과 배치 멱등성

계층별 요율을 다르게 적용하면 반올림 처리가 예상보다 결과에 영향을 미친다. 거래 1건당 원 단위 차이가 생겨도 월 수십만 건이 쌓이면 합산값이 달라진다. 어느 방향으로 반올림하느냐가 분쟁의 씨앗이 되기도 한다.

일반적으로 수수료를 내는 쪽에 유리하게, 즉 내림(floor)으로 처리한다. 채널이 플랫폼에 내는 수수료를 올림으로 계산하면 항상 실제보다 조금씩 더 내는 구조가 되고, 누적되면 근거 없이 과납한 금액이 생긴다. 플랫폼이 채널에 지급하는 정산금은 추가 올림 없이 계산된 값 그대로 내보내는 게 맞다. 이 원칙을 코드베이스 전체에서 일관되게 적용해야 한다. 한 곳만 ceil로 구현돼 있어도 나중에 역추적이 힘들어진다.

멱등성은 정산 배치에서 타협할 수 없는 속성이다. 같은 기간을 두 번 실행해도 결과가 달라지면 안 된다. 운영하다 보면 "어제 배치가 실패했으니 다시 돌려달라"는 요청이 반드시 생기는데, 이때 그냥 재실행하면 이미 처리된 건에 중복이 발생할 수 있다. 배치 실행 기록을 남기고, 정산 완료된 기간에 대한 재실행 요청은 기존 결과를 반환하거나 명시적으로 차단해야 한다. 재처리가 필요한 경우에는 '취소 후 재생성' 흐름을 별도로 두는 게 안전하다. 덮어쓰기 방식은 감사 추적이 끊기고, 그 시점의 원본을 나중에 복원할 방법이 없다.

이번 작업 규모 자체는 크지 않았다. 채널 포털 기능 추가와 일괄 등록 검증. 그런데 요율 역전 하나가 정산 배치에서 음수 마진을 만들어내고, 그게 출금 요청까지 이어지면 수습 공수가 몇 배다. 입력 단계에서 명확한 오류 메시지로 막아주는 게 가장 싸게 먹히는 방어다. 같은 크기의 수정이라도 어디서 막느냐가 전혀 다른 결과를 만든다.

댓글 0

첫 댓글 달아줘.