연락처 송금 핸들러 자가학습
목차
연락처 송금 핸들러는 시스템에서 가장 손이 많이 가는 부분 중 하나다. 은행 웹 UI는 예고 없이 바뀌고, 바뀌는 순간 셀렉터가 터지면서 이체가 멈춘다. 이번 작업은 404_pjt에 흩어져 있던 연락처 송금 비즈니스 로직을 pg-solution 쪽으로 동기화하면서, 셀렉터 자가학습 구조를 함께 정리한 것이다.
흐름 구조와 추상 핸들러
전체 연락처 송금 흐름은 아래와 같다.
입금 알림 수신 (Android 앱)
→ 서버로 원본 메시지 전송
→ 주문 매칭 (금액 + 발신자 + 시간)
→ 은행 핸들러 실행 (Playwright)
→ 로그인 → 수취인 입력 → 이체 → 완료 감지
→ 주문 상태 업데이트
→ 알림 발송
각 은행 핸들러는 추상 클래스를 상속받아 구현한다. 은행마다 UI 구조가 다르기 때문에 셀렉터가 다르고, 같은 은행이라도 앱 업데이트 이후 클래스명이나 DOM 계층이 바뀌는 경우가 있다. 이걸 하드코딩으로 박아두면 배포할 때마다 수동으로 확인해야 한다.
그래서 성공한 이체 케이스에서 사용된 셀렉터를 DB에 저장하고, 다음 실행 시 저장된 셀렉터를 우선 시도한 뒤 실패하면 fallback 탐색을 돌리는 방식으로 붙였다. 대략 이런 구조다.
class BankHandler(ABC):
@abstractmethod
async def login(self, page: Page) -> None: ...
@abstractmethod
async def transfer(self, page: Page, target: TransferTarget) -> None: ...
async def resolve_selector(self, key: str, page: Page) -> str:
cached = await self.selector_cache.get(key)
if cached and await page.locator(cached).count() > 0:
return cached
# fallback: DOM 탐색 또는 휴리스틱
found = await self._discover_selector(key, page)
await self.selector_cache.set(key, found)
return found
async def on_success(self, key: str, selector: str) -> None:
# 성공한 케이스만 캐시에 확정 저장
await self.selector_cache.confirm(key, selector)
성공한 케이스만 확정 저장한다는 게 포인트다. 탐색 중 우연히 맞은 셀렉터를 바로 굳혀버리면 다음 번에 오히려 틀릴 수 있다. 이체 완료 감지까지 성공했을 때만 confirm을 호출하는 방식으로 노이즈를 걸러낸다.
수수료 분배와 정산 로직
수수료 분배는 유통 계층 구조에 따라 차액을 위로 전달하는 방식이다. 각 계층이 설정한 요율 차이가 해당 계층의 수익이 된다. 예를 들어 최상위가 3%, 중간이 2%, 말단이 1.5%를 부과한다면, 각 계층은 차액만큼만 가져간다.
| 계층 | 적용 요율 | 차액(수익) |
|---|---|---|
| 최상위 | 3.0% | 1.0%p |
| 중간 | 2.0% | 0.5%p |
| 말단 | 1.5% | 1.5%p |
이 구조에서 원 단위 반올림 처리가 생각보다 중요하다. 수수료를 내는 쪽은 내림, 받는 쪽은 올림 없이 딱 맞게 받는 방향으로 처리하면 분쟁 소지를 줄일 수 있다. 계층이 여러 개일 때 중간에서 올림을 한 번 적용하면 말단까지 전달되는 금액이 달라지기 때문에, 어느 방향으로 튈지 코드를 직접 확인하지 않으면 감이 안 잡힌다. 이번에 동기화하면서 404_pjt와 pg-solution 쪽 반올림 방향이 한 군데 달랐던 걸 잡았다. 금액 차이는 건당 몇 원이지만, 정산 배치가 돌고 나면 합산에서 틀어지는 구조였다.
정산 배치는 멱등성이 기본 전제다. 같은 기간을 두 번 실행했을 때 결과가 달라지면 안 된다. 재처리 시나리오, 장애 후 재시도 등 운영 중에 배치를 여러 번 돌리게 되는 경우가 반드시 생기는데, 이때 멱등하지 않으면 숫자가 두 번 찍힌다. 처리 전에 해당 기간 정산 레코드가 이미 있는지 확인하고, 있으면 덮어쓰거나 건너뛰는 방식으로 처리하면 된다.
정산 UI는 숫자 가독성이 핵심이다. 총 거래액, 수수료, 실수령액을 명확하게 보여주는 구조가 기본이고, 천 단위 구분자와 '원' 단위를 일관되게 유지하는 게 전제다. 마이너스 금액은 빨간색으로 구분해두면 직관적으로 읽힌다. 이런 디테일이 사소해 보이지만, 숫자를 매일 들여다보는 운영자 입장에서는 한눈에 읽히는 화면이 훨씬 중요하다.
작업 규모보다 누적 효과
이번 작업 자체가 크지는 않았다. 로직 동기화, 셀렉터 자가학습 구조 정리, 반올림 방향 불일치 수정 정도였다. 그런데 이런 류의 수정이 쌓이지 않으면 나중에 더 큰 문제로 터진다.
Playwright 기반 은행 핸들러는 외부 UI 변화에 항상 노출되어 있다. 자가학습 구조는 그 리스크를 완전히 없애지는 못하지만, 사람이 매번 수동으로 셀렉터를 업데이트해야 하는 의존도를 낮춰준다. 성공 케이스가 쌓일수록 캐시 적중률이 올라가고, 새 은행 핸들러를 추가할 때도 공통 탐색 로직을 그대로 쓸 수 있다.
수수료 계산은 금전 관련이라 오류가 나면 신뢰 손상이 바로 따라온다. 계층별 요율이 DB로 관리되고 있어서 비율 자체를 바꾸는 건 코드 배포 없이도 가능한 구조인데, 그만큼 수동 설정 실수가 들어올 여지도 있다. 배치 실행 전에 요율 합산이 100%를 넘지 않는지, 계층 순서가 맞는지 사전 검증 로직을 한 번 더 넣어두는 게 낫겠다 싶었다. 이건 다음 배포 때 추가할 것들 목록에 넣어뒀다.
댓글 0
첫 댓글 달아줘.