개발 slecs

채널 계층 요율 검증과 일괄 등록으로 정산 안정성 강화

목차

채널 포털 작업은 외부 채널 사업자들이 직접 들어와서 자기 현황을 확인하고 설정을 만지는 공간이다. 잔액, 거래 내역, 하위 채널 현황 같은 것들을 대시보드 하나에서 볼 수 있게 해두는 구조인데, 이게 생각보다 신경 써야 할 부분이 많다. 계층이 얽혀 있는 데다 금전이 오가는 수수료 로직이 붙어 있어서다.

이번 작업의 핵심은 두 가지였다. 일괄 등록 기능 추가, 그리고 계층 요율 검증 강화. 규모 자체는 크지 않았지만 정산 안정성 쪽에서는 꽤 의미 있는 변경이었다고 본다.

계층 요율 검증이 왜 필요한가

채널 구조는 상위-하위로 이어지는 트리 형태다. 상위 채널이 플랫폼으로부터 받는 요율이 있고, 하위 채널에는 그보다 높은 요율을 적용해서 그 차이로 마진을 챙기는 구조. 단순해 보이지만 등록 단계에서 이 순서를 잘못 입력하면 마진이 음수가 나온다. 시스템은 모르고 계산을 계속 돌리고, 정산이 틀어지고 나서야 발견되는 게 전형적인 패턴이다.

기존에는 단건 등록 시에만 일부 검증이 들어가 있었고, 일괄 등록 경로에는 이 체크가 빠져 있었다. 엑셀 업로드로 수십 개 채널을 한 번에 밀어 넣을 때 요율 역전이 섞여 들어오면 후처리가 복잡해진다. 이번에 업로드 시점에 계층 검증을 추가해서, 요율 순서가 맞지 않으면 오류와 함께 어떤 행이 문제인지 바로 반환하도록 처리했다.

검증 규칙은 간단하다.

부모 요율 < 자식 요율  → 정상
부모 요율 >= 자식 요율 → 마진 0 이하, 오류 반환

코드로는 업로드된 채널 목록을 순회하면서 각 채널의 parentId를 타고 올라가 요율 비교를 한다. DB에서 상위 채널을 조회하는 횟수가 늘어날 수 있어서 업로드 전처리 단계에서 상위 채널을 미리 맵으로 캐시해두는 방식을 썼다.

parent_map = {ch["id"]: ch for ch in fetch_channels(parent_ids)}

for row in upload_rows:
    parent = parent_map.get(row["parent_id"])
    if parent and row["rate"] <= parent["rate"]:
        errors.append({
            "row": row["index"],
            "msg": f"요율 역전: 상위({parent['rate']}) >= 입력값({row['rate']})"
        })

오류가 하나라도 있으면 전체를 롤백한다. 부분 성공을 허용하면 어느 행이 반영됐는지 추적이 어려워지고, 운영 입장에서는 그게 더 피곤하다.

정산 UI와 숫자 표기 일관성

채널 사업자들이 포털에서 가장 자주 보는 화면은 결국 정산 내역이다. 총 거래액, 수수료, 실수령액. 이 세 숫자가 명확하지 않으면 문의가 들어온다.

기존에 천 단위 구분자가 빠진 곳이 일부 있었고, 화면마다 표기 방식이 조금씩 달랐다. 이번에 정산 관련 컴포넌트를 손보면서 아래 규칙을 통일했다.

항목 적용 규칙
금액 단위 원(KRW), 천 단위 콤마
마이너스 금액 빨간색 텍스트, - 부호 명시
소수점 이하 버림 처리(사용자 불리 방향 제거)
0원 거래 별도 색상 없이 동일 처리

마이너스 표기는 실수로 빨간색 처리를 빠뜨리면 환불이나 차감이 일반 금액처럼 보여서 혼란을 준다. 작은 것 같지만 운영 문의로 들어오는 빈도를 보면 무시할 수 없는 부분이다.

올림/버림 처리는 수수료 계산에서 항상 나오는 주제다. 원 단위 소수점이 생기면 어느 방향으로 반올림하느냐에 따라 사업자와 플랫폼 사이에서 몇 원씩 차이가 쌓인다. 일반적인 방향은 수수료를 내는 쪽에 유리하게, 즉 수수료는 내림으로 덜 내고 실수령액은 그 결과를 따라가는 방식이다. 기준을 문서화해두지 않으면 로직 수정 때마다 방향이 흔들린다.

정산 배치 멱등성과 FCM 재시도

정산 배치는 같은 기간을 두 번 실행해도 결과가 같아야 한다. 멱등성이 보장되지 않으면 실수로 재실행했을 때 이중 정산이 생기고, 그걸 되돌리는 데 드는 비용이 처음 제대로 짜는 것보다 훨씬 크다.

이번에 상위 파트너 설정과 URL 캡처 실패 시 FCM 재시도 기능도 추가했다. 외부 채널 포털에서 특정 액션이 발생했을 때 FCM 푸시로 알림을 보내는데, URL 캡처 단계에서 간헐적으로 실패가 있었다. 재시도 없이 그냥 넘어가면 알림이 유실된다. 단순한 재시도라도 멱등 키를 붙여서 동일한 이벤트로 알림이 두 번 가는 상황을 막는 게 기본이다.

def send_fcm_with_retry(token, payload, idempotency_key, max_retries=3):
    for attempt in range(max_retries):
        if already_sent(idempotency_key):
            return  # 중복 발송 차단
        try:
            fcm_client.send(token, payload)
            mark_sent(idempotency_key)
            return
        except UrlCaptureError:
            if attempt == max_retries - 1:
                log_failure(idempotency_key, payload)
            time.sleep(2 ** attempt)

재시도 로직이 있다고 해서 모든 실패를 잡아주진 않는다. FCM 토큰 자체가 만료됐거나 기기 문제면 재시도해도 소용없고, 이 경우는 실패 로그를 남기고 넘어가는 게 맞다. 재시도 한도를 너무 높이면 외부 서비스 장애 때 요청이 쌓여서 더 큰 문제로 이어질 수 있어서 3회 정도로 제한했다.

작업 규모 자체는 크지 않았다. 하지만 계층 요율 역전, 정산 배치 멱등성, FCM 재시도 같은 것들은 터지고 나서 고치면 항상 늦다. 평소 조용한 이유가 이런 예방 작업들이 쌓여 있기 때문이라고 생각하면 기분은 좀 낫다.

댓글 0

첫 댓글 달아줘.