연락처 송금 버그 수정으로 결제 자동화 안정성 개선
목차
연락처 송금은 말이 좋아서 '자동화'지, 실제로는 은행 앱 화면을 Playwright로 긁어가며 버튼을 눌러 입금을 완료하는 구조다. 각 은행마다 UI가 제각각이라 핸들러도 은행별로 따로 존재하고, 그 핸들러들이 셀렉터 기반으로 화면 요소를 찾는다. 이번에 수정한 버그 4개는 각각 독립적인 이슈였지만, 같은 레이어에 몰려 있었다.
이번 수정에서 건드린 것들
수정 항목을 하나씩 들여다보면 다음과 같다.
| 항목 | 증상 | 원인 |
|---|---|---|
| 은행 셀렉터 감지 오류 | 특정 은행 핸들러가 요소를 못 찾음 | 셀렉터 우선순위 로직이 잘못된 분기 탔음 |
| 계좌 로테이션 순서 | 특정 계좌에 요청이 몰림 | 순번 인덱스가 초기화 시점에 리셋되지 않음 |
| 재시도 필터 조건 누락 | 파싱 실패 건이 무한 재시도 루프에 진입 | 실패 원인 코드별 필터가 빠져 있었음 |
| 비회원 주문 매칭 누락 | 비회원 결제가 NO_MATCH로 처리됨 | 매칭 쿼리가 회원 ID 기준으로만 조회 |
계좌 로테이션 문제는 오래된 이슈였다. 로테이션 인덱스를 서버 메모리에 들고 있어서 재시작 시 항상 0번 계좌부터 시작했고, 배포가 잦은 구간에는 특정 계좌에 요청이 쏠리는 패턴이 반복됐다. 이번에 인덱스를 DB에 영속화하는 방식으로 바꿨다.
비회원 주문 매칭은 솔직히 처음부터 고려가 안 됐던 거다. 회원 주문 기준으로 매칭 로직을 짰고, 비회원 케이스가 들어올 때 NO_MATCH 상태로 빠져서 수동 처리 큐에 계속 쌓이고 있었다.
완료 감지 로직과 AI 폴백
완료 여부를 판단하는 게 이 시스템에서 제일 까다로운 부분이다. 은행마다 완료 화면 문구가 다 다르다. "이체가 완료됐습니다", "송금이 처리됐습니다", "성공적으로 완료", 어떤 건 그냥 금액 확인 화면만 보여주는 경우도 있다.
기본은 정규식이다.
// 완료 감지 regex 통합 패턴
private static final Pattern COMPLETE_PATTERN =
Pattern.compile("(이체|송금|완료|처리됐|성공)");
이 패턴으로 잡히지 않는 케이스는 Claude API로 화면 텍스트를 넘긴다. 화면 전체 텍스트를 그대로 던지고, 이게 이체 완료 화면인지 판단 결과를 받는 구조다. 처음에는 이게 너무 과한 방법 아닌가 싶었는데, 실제로 돌려보면 은행 앱이 워낙 문구를 자유롭게 바꾸다 보니 regex edge case가 계속 나왔다. LLM한테 "이 텍스트가 이체 완료 화면이면 true 반환해줘"라고 시키는 게 오히려 유지보수가 편하다.
폴백 구조를 명시적으로 표현하면 이렇다.
1차: COMPLETE_PATTERN.matcher(screenText).find()
└ 실패 →
2차: Claude API로 screenText 전달 → boolean 반환
└ 실패 or 타임아웃 →
MANUAL_REQUIRED 처리
응답 타임아웃은 2초로 잡았다. 그 이상 넘어가면 자동화 이점이 없어서다.
셀렉터 자가학습 캐시
은행 앱 UI 변경이 이 시스템 장애의 주범이다. 앱 업데이트 한 번에 버튼 클래스명이 바뀌거나 레이아웃이 달라지면 기존 셀렉터가 통째로 무용지물이 된다. 모니터링 없이 그냥 두면 조용히 처리 실패가 쌓인다.
이걸 완화하는 게 셀렉터 캐시 구조다. 요청이 성공하면 그 시점에 동작한 셀렉터를 DB에 저장한다. 다음 요청은 저장된 셀렉터를 먼저 시도하고, 실패하면 전체 DOM 탐색으로 폴백한다. 탐색에서 새로운 셀렉터를 찾으면 다시 저장한다.
이 흐름 덕분에 은행이 앱 업데이트를 해도 처음 실패한 한 건만 탐색 비용을 쓰고 이후 건들은 캐시 히트로 처리된다. 완전한 해결은 아니지만 장애 파급을 한 건으로 국한할 수 있다.
처리 상태는 4개 값으로 관리한다.
PENDING- 처리 진행 중SUCCESS- 완료 확인됨NO_MATCH- 주문 매칭 실패, 재시도 큐로MANUAL_REQUIRED- 자동 처리 불가, 수동 처리 대상
재시도 필터 수정이 여기서 연결된다. 이전에는 NO_MATCH와 파싱 실패가 같은 재시도 로직을 탔다. NO_MATCH는 다음 입금 사이클에 다시 매칭을 시도해야 하고, 파싱 실패는 즉시 재시도하거나 MANUAL_REQUIRED로 전환해야 한다. 원인 코드가 없이 단순 실패로만 처리하다 보니 파싱 실패 건이 재시도 큐에서 계속 순환하고 있었다.
작업 규모는 크지 않았다. 하루 안에 전부 들어간 수정들이다. 그런데 이런 류의 버그는 특정 케이스에서만 조용히 실패하기 때문에 모니터링 로그를 꼼꼼히 안 보면 모르고 지나가기 쉽다. 실패 상태별 카운트를 대시보드에 올려두고 주기적으로 보는 게 이런 이슈를 조기에 잡는 데 실질적으로 도움이 됐다.
댓글 0
첫 댓글 달아줘.