개발 slecs

연락처송금 자동이체와 수수료 정산 화면 개선

목차

3월 마지막 주 작업이라 규모 자체는 작았는데, 건드린 도메인이 금전 처리 쪽이라 손을 댈 때마다 긴장이 따라온다. 연락처 송금 자동이체 흐름을 손보고, 그 위에 얹히는 수수료 정산 화면도 같이 정리했다.

연락처 송금 자동이체 구조

연락처 송금의 전체 흐름은 이렇게 이어진다.

입금 알림 수신 (Android 앱)
  → 서버로 원본 메시지 전송
  → 주문 매칭 (금액 + 발신자 + 시간)
  → 은행 핸들러 실행 (Playwright)
    → 로그인 → 수취인 입력 → 이체 → 완료 감지
  → 주문 상태 업데이트
  → 알림 발송

각 단계가 직렬로 이어지기 때문에 중간에 뭔가 끊기면 주문이 대기 상태로 걸려버린다. 특히 Playwright 기반 은행 핸들러 쪽이 불안정할 때 문제가 생기는 경우가 잦았다. 은행 앱 업데이트 한 번에 셀렉터가 통째로 바뀌는 일이 종종 있어서, 예전엔 이걸 수동으로 업데이트해야 했다.

지금 구조는 은행별 핸들러가 추상 클래스를 상속받아서 구현하는 방식이다. 공통 로직(로그인 시도, 완료 감지, 상태 업데이트)은 베이스에 두고, 은행마다 UI가 다른 부분만 각 구현체에서 오버라이드한다. 여기에 셀렉터를 DB에 캐시해두고 성공한 케이스를 자가학습하는 구조를 붙였다.

class BaseBankHandler:
    def execute(self, order):
        self.login()
        self.fill_recipient(order)
        self.confirm_transfer()
        return self.detect_completion()

    def fill_recipient(self, order):
        selector = self._get_cached_selector("recipient_input")
        # 서브클래스에서 오버라이드 가능
        self.page.fill(selector, order.recipient)

    def _get_cached_selector(self, key):
        cached = db.get_selector(self.bank_code, key)
        return cached or self.FALLBACK_SELECTORS[key]

    def _on_success(self, selector_key, selector_value):
        db.upsert_selector(self.bank_code, selector_key, selector_value)

셀렉터 자가학습의 핵심은 "성공한 시점의 셀렉터를 기록한다"는 단순한 원칙이다. 실패한 시도는 무시하고, 완료 감지까지 통과한 케이스만 DB에 반영한다. 이렇게 하면 은행 UI가 조용히 바뀌어도 성공 사례가 한 건만 생기면 이후부터는 새 셀렉터로 자동 전환된다. 물론 처음 깨지는 그 한 건은 수동 개입이 필요하지만, 이전처럼 배포 없이 DB만 고쳐서 복구할 수 있다는 게 실운영에서 꽤 차이가 난다.

수수료 계층 정산과 반올림 정책

수수료 분배는 유통 계층 구조에 따라 차액을 위로 올리는 방식이다. 각 계층이 설정한 요율 차이가 그 계층의 수익이 된다. 예를 들어 최하위 대리점이 고객에게 2.5%를 받고, 그 위 총판에 2.0%를 내는 구조라면 0.5%가 대리점 몫이다.

계층 고객 요율 상위 납부 요율 계층 수익
대리점 2.5% 2.0% 0.5%
총판 2.0% 1.5% 0.5%
본사 1.5% - 1.5%

이 구조에서 반올림 처리를 잘못 하면 금액이 계층을 거칠수록 누적 오차가 생긴다. 일반적으로 원 단위 정산은 수수료를 내는 쪽에 유리하게, 즉 내림 처리를 기본으로 가져간다. 덜 내는 쪽이 아니라 더 내는 쪽에서 반올림이 발생하면 분쟁 소지가 생기기 때문이다. 수수료를 수령하는 상위 계층 입장에서는 소수점이 잘려 조금 덜 받는 게 결과적으로 더 깔끔하다.

정산 배치는 멱등성이 핵심이다. 같은 기간을 두 번 돌려도 결과가 달라지면 안 된다. 배치 실행 단위마다 처리 여부를 플래그로 남기고, 이미 처리된 건은 건너뛰도록 했다. 회계 배치에서 멱등성이 깨지면 중복 지급이나 누락이 생기는데, 이걸 나중에 대사(reconciliation)로 잡으려 하면 공수가 몇 배로 늘어난다. 처음부터 막는 게 훨씬 싸다.

-- 이미 정산된 기간은 재삽입되지 않도록
INSERT INTO settlement_records (period, layer_id, amount, created_at)
SELECT :period, :layer_id, :amount, NOW()
WHERE NOT EXISTS (
  SELECT 1 FROM settlement_records
  WHERE period = :period AND layer_id = :layer_id
);

정산 UI 개선

정산 화면에서 사용자가 실제로 신경 쓰는 건 딱 세 가지다. 총 거래액, 수수료, 실수령액. 나머지는 드릴다운으로 숨겨도 된다.

금액 표기는 천 단위 구분자와 '원' 단위를 일관되게 붙이는 게 기본이다. 한 화면 안에서 10000001,000,000원이 섞이면 사용자가 숫자를 더블체크하게 된다. 마이너스 금액은 빨간색 처리 하나만으로 직관성이 확 올라간다. 이건 UI 리서치를 거칠 것도 없이, 인터넷뱅킹이 수십 년간 쌓은 컨벤션을 그냥 따르면 된다.

이번에 정산 화면에서 손본 부분도 그 방향이었다. 계층별 요율을 나란히 보여주고, 각 항목이 어떻게 계산됐는지 한 줄 설명을 붙이는 것만으로 CS 문의가 줄어든다. "왜 이 금액이 나왔냐"는 질문은 대부분 화면이 안 알려줘서 생기는 질문이다.

작업 규모 자체는 크지 않았다. 그래도 금전 처리 쪽은 작은 수정 하나가 누적 오차나 신뢰 손상으로 이어지기 쉬워서, 이런 정비를 미루면 나중에 더 큰 비용으로 돌아온다. 반올림 정책 하나, 멱등성 처리 하나, 화면 표기 하나씩 정리해두는 게 결국 시스템 전체의 신뢰도를 받쳐준다.

댓글 0

첫 댓글 달아줘.