연락처 송금 계좌 로테이션·비회원 주문 매칭 버그 수정
목차
연락처 송금은 구조 자체가 독특한 편이다. 사용자가 은행 앱을 직접 건드리지 않아도 입금이 완료되도록, 시스템이 Playwright로 브라우저를 띄워서 은행 앱 UI를 직접 제어한다. API 한 번 호출해서 끝나는 방식이 아니라, 화면을 보면서 버튼을 누르고 결과를 읽는 흐름 전체를 자동화하는 구조다.
문제는 은행마다 UI가 제각각이라는 것. 그래서 은행별 핸들러를 개별로 유지하고 있다. A 은행은 이체 완료 팝업이 특정 div 안에 뜨고, B 은행은 전혀 다른 경로로 확인 페이지가 렌더링된다. 이 다양성을 커버하기 위해 셀렉터 감지 로직과 AI 폴백이 함께 물려 있는 구조다.
이번 버그는 그 중 네 군데에서 동시에 발견됐다.
수정한 버그들, 하나씩
은행 셀렉터 감지 오류는 비교적 단순한 케이스였다. 셀렉터 탐색 순서가 잘못 설정돼 있어서, 실제 화면에서 먼저 나타나는 요소가 아닌 다른 요소를 먼저 시도하고 있었다. 탐색에 실패하면 폴백을 타는데, 폴백을 탄다는 것 자체가 응답 시간이 늘어난다는 뜻이라 운영 중에 체감 지연이 있었다.
계좌 로테이션 순서 문제는 좀 더 중요한 케이스였다. 연락처 송금에서 여러 계좌를 순환해서 쓰는 이유는 단일 계좌 집중을 막기 위해서다. 그런데 로테이션 순서 로직이 잘못돼서 특정 계좌가 반복적으로 선택되는 상황이 생겼다. 균등 분산이 목적인데 실제로는 불균등하게 몰리고 있었던 것이다.
파싱 실패 후 재시도 필터 조건 누락이 이번 수정 중 가장 조용하지만 위험한 부분이었다. 재시도 큐를 처리할 때 이미 처리 중인 건(PENDING 상태)을 필터링하지 않고 있었다. 파싱 실패로 큐에 들어간 항목이 PENDING 상태임에도 다시 잡혀서 중복 처리가 시도될 수 있는 구조였던 것이다. 실제로 중복 입금이 완료된 케이스는 없었지만, 이 조건 하나 누락이 타이밍에 따라 큰 문제로 번질 수 있었다.
비회원 주문 매칭 누락은 쿼리 조건 문제였다. 입금 알림이 왔을 때 금액, 발신자, 시간 조건으로 주문을 찾는데, 해당 쿼리가 회원 기반 주문만 보고 있었다. 비회원 주문은 아예 검색 대상에서 빠져 있어서 매칭이 안 되면 NO_MATCH로 처리됐다. 비회원 주문 비율이 낮더라도 실제 입금이 처리 안 되는 건이라 곧바로 수동 처리로 넘어갔고, 그게 꽤 오래된 구멍이었다.
완료 감지 정규식 패턴은 현재 이렇게 묶여 있다.
// 완료 감지 regex 통합 패턴
private static final Pattern COMPLETE_PATTERN =
Pattern.compile("(이체|송금|완료|처리됐|성공)");
이걸로 커버 안 되는 케이스가 있다. 은행마다 완료 메시지 문구가 미묘하게 다른데, 일부 은행은 "출금 처리가 완료되었어요" 같은 구어체로, 또 어떤 곳은 "이체 요청이 접수됐습니다"처럼 완료와 접수를 구분하는 표현을 쓴다. 이런 케이스는 regex 히트가 없으면 Claude API에 화면 텍스트를 그대로 넘겨서 완료 여부 판단을 맡긴다. 은행 특성을 일일이 핸들러에 하드코딩하는 것보다 유지보수 면에서 훨씬 낫다.
셀렉터 캐싱 전략과 트레이드오프
은행 앱 UI 자동화의 가장 큰 리스크는 앱 업데이트다. 셀렉터를 하드코딩해두면 은행이 앱 버전을 올리는 순간 그냥 멈춘다. 이 문제를 완화하기 위해 성공한 셀렉터를 DB에 저장해두는 자가학습 캐시 구조를 쓰고 있다.
흐름은 이렇다.
| 단계 | 동작 |
|---|---|
| 1 | 저장된 셀렉터로 먼저 시도 |
| 2 | 실패 시 전체 DOM 탐색으로 폴백 |
| 3 | 새 셀렉터 발견 시 DB 저장, 다음 요청부터 활용 |
| 4 | 탐색 자체 실패 시 AI 폴백으로 화면 텍스트 파싱 |
트레이드오프는 명확하다. 첫 요청은 캐시 미스로 인해 전체 탐색 비용이 들고, 이후 요청부터 빨라진다. 앱 업데이트로 캐시가 무효화됐을 때도 결국 폴백을 타고 새 셀렉터를 재학습하기 때문에 운영 중단 없이 이어갈 수 있다. 다만 은행 UI가 바뀐 직후 첫 번째 요청이 AI 폴백까지 내려가면 응답이 느리다. 이건 허용되는 수준으로 보고 있다.
상태 관리는 네 가지다.
PENDING- 처리 중. 이 상태인 항목은 재시도 큐에서 반드시 제외해야 함SUCCESS- 완료NO_MATCH- 주문 매칭 실패, 재시도 큐로 이동MANUAL_REQUIRED- 자동 처리 불가, 수동 처리로 에스컬레이션
이번 수정에서 PENDING 필터링 조건 추가와 비회원 주문을 매칭 쿼리에 포함시킨 게 핵심이었다. 코드 변경 규모는 크지 않았다. 그런데 이런 류의 수정이 방치됐을 때 생기는 결과는 규모에 비해 크다. 필터 조건 하나가 중복 처리로, 쿼리 범위 하나가 매칭 누락으로 이어지고, 그게 수동 처리 건수 증가로 직결된다. 자동화의 신뢰도는 결국 이런 엣지 케이스를 얼마나 촘촘하게 막았느냐로 결정되는 것 같다. 이번 수정이 다시 한번 그걸 확인시켜줬음.
댓글 0
첫 댓글 달아줘.