개발 slecs

연락처 송금 버그 수정과 비회원 주문 매칭 누락 해결

목차

연락처 송금 자동화는 구조적으로 취약할 수밖에 없다. 은행 앱 화면을 Playwright로 직접 조작하는 방식이다 보니, 앱 업데이트 한 번에 셀렉터가 통째로 깨진다. 그걸 알면서도 쓰는 이유는 단순한데, 은행 공식 API로는 이 흐름을 구현할 수 없기 때문이다. RPA에 가까운 접근이고, 편법이지만 현실적인 선택이다.

팀장 포지션에서 이런 코드를 직접 디버깅하는 게 맞냐 싶을 수도 있는데, 결제 흐름은 건드리는 사람이 시스템을 속속들이 알고 있어야 한다는 게 내 생각이다. 누가 짠 코드든 이 레이어는 내가 들여다보는 편이 낫다. 이번처럼 조용히 쌓이던 버그들이 어느 시점에서 한꺼번에 터지기 전에 먼저 잡는 게 낫고.

이번에 수정한 버그는 크게 네 가지였다. 각각 독립적으로 작은 문제처럼 보이지만, 전부 같은 코드 경로에 걸쳐 있었다.

버그 수정 내용

은행 셀렉터 감지 로직 오류부터 짚고 넘어가자. 각 은행은 Playwright로 핸들링하는 개별 핸들러가 있다. 은행마다 앱 UI 구조가 다르니 핸들러를 분리하는 건 당연한 설계다. 문제는 어느 핸들러를 활성화할지 판단하는 감지 로직이었다. A 은행 핸들러가 먼저 실행되면서 B 은행 화면을 잘못 잡는 케이스가 생겼다. 셀렉터 매칭 조건의 특이도(specificity)를 기준으로 순서를 재정렬하고, 각 핸들러가 자신 아닌 은행을 잘못 잡지 않도록 조건을 더 엄격하게 걸었다.

계좌 로테이션 순서 문제는 여러 계좌를 순번 돌려가며 쓰는 구조에서 생겼다. 이전 요청의 상태가 완전히 클리어되지 않은 채 다음 계좌로 넘어가는 경우가 있었다. 락 해제와 인덱스 전진 순서를 바꿨다. 별거 아닌 것 같지만, 이 구조에서 상태 오염이 생기면 같은 계좌로 동시에 두 요청이 물릴 수 있고, 그게 중복 이체로 이어지는 경로다.

파싱 실패 후 재시도 필터 조건 누락은 알면서 미뤄두고 있던 부분이었다. 이체 완료 감지에 실패한 건들을 재시도 큐에 넣는데, 이미 SUCCESS로 처리된 건이 필터링에서 빠져서 중복 재시도 대상이 될 수 있었다. 재시도 조건에 상태값 체크를 추가해서 막았다. 중복 이체 리스크가 이 경로에서도 생길 수 있어서 방치하면 안 됐다.

완료 감지 자체는 regex와 AI 폴백, 두 단계로 구성된다.

// 완료 감지 regex 통합 패턴
private static final Pattern COMPLETE_PATTERN =
    Pattern.compile("(이체|송금|완료|처리됐|성공)");

1차로 이 패턴으로 화면 텍스트를 검사하고, 걸리지 않으면 화면 전체 텍스트를 Claude API로 넘긴다. 은행마다 "이체가 완료되었어요", "송금 성공!", "처리가 완료됐습니다" 같은 식으로 문구가 제각각이고, 앱 버전이 올라가면 또 바뀐다. regex 하나로 모든 경우를 커버하려면 패턴이 너무 느슨해지거나 오탐이 생긴다. AI 폴백이 이 갭을 메워준다. 구조가 약간 복잡해지지만, 실운영에서 완료 감지 누락이 생기는 것보다는 낫다.

비회원 주문 매칭 누락

이번 수정 중에서 실제 운영 임팩트가 가장 컸던 건 이 부분이다.

입금 알림이 들어오면 금액, 발신자명, 시간 조건으로 주문을 매칭한다. 동일 금액 주문이 여러 건이면 가장 최근 주문을 우선 처리한다. 그런데 매칭 쿼리가 회원 기반으로만 조회하고 있어서, 비회원 주문이 아예 매칭 대상에서 빠져 있었다.

-- 수정 전: 회원 주문만 조회
WHERE user_id IS NOT NULL
  AND amount = :amount
  AND created_at >= :threshold

-- 수정 후: 비회원 포함
WHERE amount = :amount
  AND created_at >= :threshold

비회원 주문은 user_id가 null이라 IS NOT NULL 조건에 걸려버렸다. 발견이 늦었던 이유는 비회원 주문 비율이 상대적으로 낮다 보니 지표에서 잘 안 드러났기 때문이다. 모니터링 알림에 NO_MATCH가 몇 건씩 쌓여도 "원래 이런저런 이유로 매칭 안 되는 건" 취급으로 묻혔다.

이런 버그가 무서운 건, 돈 관련 프로세스에서 소리 없이 실패가 나기 때문이다. 입금은 됐는데 주문이 안 잡히면 운영팀이 수동으로 처리해야 한다. 트랜잭션 볼륨이 올라갈수록 수동 처리 부하가 선형으로 늘어나고, 그게 쌓이면 실제 고객 불만으로 이어진다. 작은 쿼리 조건 하나가 운영 부하와 직결이다.

셀렉터 자가학습 캐시와 상태 관리

은행 앱 UI 변경에 대한 구조적 대응으로 자가학습 캐시가 붙어 있다. 성공한 셀렉터를 DB에 저장해두고, 다음 요청에서는 저장된 셀렉터를 먼저 시도한다. 실패하면 전체 탐색으로 폴백하고, 이번에 성공한 새 셀렉터를 다시 저장한다. 앱 버전이 바뀌어서 기존 셀렉터가 깨지더라도, 한 번 전체 탐색으로 새 셀렉터를 찾고 나면 이후 요청은 다시 캐시 히트로 처리된다.

장애가 첫 번째 요청에서 한 번 나고, 이후엔 자동 복구되는 구조다. 이게 없으면 앱 업데이트마다 셀렉터를 수동으로 수정해서 배포해야 한다. 지원 은행이 늘어날수록 이 유지보수 비용이 감당이 안 된다.

처리 상태는 네 가지로 관리한다.

상태 의미 다음 처리
PENDING 처리 중 -
SUCCESS 완료 종료
NO_MATCH 주문 매칭 실패 재시도 큐
MANUAL_REQUIRED 자동 처리 불가 운영팀 알림

NO_MATCH는 재시도 큐에서 일정 횟수 돌리고, 그래도 안 되면 MANUAL_REQUIRED로 전환된다. 이번에 재시도 필터 조건을 수정한 것도 이 상태 전환이 정확하게 작동해야 하기 때문이다. NO_MATCH가 과도하게 쌓이면 운영팀 알림이 노이즈가 되어서, 진짜 이슈를 놓치는 피로도 문제로 이어진다.

비회원 매칭 수정 이후 NO_MATCH 발생 건수가 줄어들어야 맞다. 배포 후 며칠치 지표를 확인해봐야 하는데, 이 부분은 당분간 주기적으로 챙길 생각이다.

작업 규모 자체는 크지 않았다. 파일 몇 개, 조건 몇 줄. 그런데 이런 수정들이 제때 들어가지 않으면, 자동화 시스템은 겉으로 돌아가면서 안에서 계속 새고 있는 상태가 된다. 셀렉터 자가학습이나 AI 폴백처럼 변경에 강한 구조를 갖춰두고, 거기에 남은 구멍들을 하나씩 막아가는 게 안정적인 운영의 실체다.

댓글 0

첫 댓글 달아줘.