자동화 slecs

파트너 정산 송금을 접근성 서비스로 자동화한 과정

목차

파트너 정산 송금을 접근성 서비스로 자동화하는 건 처음부터 쉬운 선택이 아니었다. 결제대행사 정산 사이클 안에 들어오는 건은 플랫폼이 알아서 처리하지만, 그 밖에서 발생하는 즉시 송금 건이 늘어나면서 운영팀이 직접 은행 앱을 두드려야 하는 횟수가 눈에 띄게 쌓였다. 건당 걸리는 시간이 짧아도 처리량이 쌓이면 얘기가 달라짐. 반복 작업이 많아질수록 실수 가능성도 같이 올라간다.

왜 접근성 서비스였나

RPA 도구와 API 직연동을 먼저 검토했다. 은행 공개 API는 대부분 개인 계좌 이체 전용이거나 기업 전용 계약이 필요한 상황이었고, 서드파티 RPA 솔루션은 라이선스 비용과 운영 오버헤드 문제가 걸렸다. 결국 안드로이드 접근성 서비스(Accessibility Service)가 현실적인 선택지였음.

접근성 서비스는 원래 시각 장애 사용자 보조 목적으로 만들어진 API다. 화면의 뷰 계층을 쿼리하고 특정 노드에 액션을 디스패치할 수 있는데, 권한 자체가 강력해서 오용되면 다른 앱 화면을 마음대로 읽거나 조작할 수 있다. 내부 운영 도구라 배포 채널 문제는 비켜갔지만, 화이트리스트를 철저히 적용해서 지정된 은행 앱 외에는 접근성 이벤트를 아예 무시하도록 설계했음.

결제 플랫폼 본 흐름은 손대지 않는 게 원칙이었다. 자동화는 사람이 수동으로 처리하던 마지막 구간, 즉 은행 앱 폼을 채우는 부분만 대신함. 최종 확인 버튼은 사람이 직접 누르게 고정했고, 자동 확인은 설계 단계에서 아예 배제했다. 금전 거래에서 자동 완결은 버그 하나로 피해가 즉각 발생하는 영역이라 이 선을 지키는 게 중요했음.

흐름 설계와 은행별 폼 매핑

메신저 알림 수신에서 시작해 은행 앱 필드 입력까지 흐름은 단순하다.

windowStateChanged 이벤트 수신
  → 화면 식별 (앱 패키지 + 액티비티 + URL)
  → 매핑 테이블 조회
  → 노드 트리에서 타겟 검색
  → SET_TEXT 액션 디스패치
  → 입력 검증 후 다음 단계

단순해 보이지만 까다로운 지점은 은행별 WebView DOM 구조가 제각각이라는 점이었음. 이체 화면이라는 같은 목적의 폼인데도 셀렉터 방식이 은행마다 달랐다. 통일을 시도했다가 포기하고 매핑 테이블로 분리했음.

은행 계좌 입력 금액 입력 메모
A input[type=tel] 첫 번째 input placeholder 매칭
B id 기반 셀렉터 name 기반 별도 화면
C resourceId resourceId 미지원

이 테이블이 핵심 추상화다. 코어 로직은 "매핑에서 셀렉터 가져와서 노드 찾고 값 넣는다"는 공통 흐름으로 고정하고, 은행 특성은 전부 테이블 안에 가뒀음. 새 은행이 들어오면 행 하나만 추가하면 됨. 코어를 건드릴 필요가 없으니 리뷰도 매핑 PR 한 장으로 끝난다.

입력 방법에도 함정이 있었다. 처음엔 ACTION_SET_TEXT만 썼는데, 일부 은행 앱 필드는 이걸로 값이 세팅돼도 내부 TextWatcher가 반응을 안 했음. 사용자가 타이핑하거나 붙여넣기할 때만 이벤트가 발생하도록 만들어진 구조였다. 클립보드에 값을 올리고 ACTION_PASTE를 디스패치하는 fallback 경로를 끼워 넣어서 해결함. SET_TEXT를 먼저 시도하고 검증 실패 시 클립보드 경로로 넘어가는 방식이다.

fun fillField(node: AccessibilityNodeInfo, value: String): Boolean {
    val bundle = Bundle().apply {
        putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
    }
    node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle)
    if (node.text?.toString() != value) {
        // fallback: clipboard paste
        clipboardManager.setPrimaryClip(ClipData.newPlainText("fill", value))
        node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
    }
    return node.text?.toString() == value
}

입력 후에 실제 노드 텍스트를 다시 읽어서 기댓값과 일치하는지 확인하는 가드를 넣었다. 침묵 실패가 가장 무서운 시나리오임. 폼이 채워진 것처럼 보이는데 실제로 값이 안 들어갔다면, 사람이 확인 버튼을 누를 때 이미 틀린 상태다. 한 단계라도 검증이 어긋나면 즉시 중단하고 알림을 쏘게 했음.

디버그 환경과 운영 안정성

접근성 서비스 자동화는 테스트가 까다롭다. 실제 은행 앱과 계좌가 필요하고, QA 환경에서 매번 진짜 메신저 메시지를 쏠 수 없음. 디버그 전용 브로드캐스트 수신기를 따로 만들었다. 매니페스트에서 빌드 타입 분기를 걸어 운영 빌드에는 이 컴포넌트 자체가 포함되지 않도록 처리했음.

  • 시나리오 파일로 다양한 계좌/금액/메모 조합을 셸 스크립트로 주입 자동화
  • 트레이스 자동 캡쳐, 로그 레벨 상승으로 노드 탐색 과정 전부 가시화
  • 권한 상태 사전 점검: 접근성 서비스 활성 여부, 클립보드 권한 확인
  • 노드 트리 덤프 모드: 새 은행 매핑 추가 시 실제 WebView DOM 구조 역추출용

노드 트리 덤프 없이 새 은행 매핑 작업하는 건 거의 불가능에 가깝다. 은행 앱 WebView 내부 구조는 브라우저 개발자 도구로 볼 수 없고, 접근성 서비스로 직접 노드 트리를 찍어봐야 셀렉터를 뽑을 수 있음.

가장 까다로웠던 건 은행 앱 업데이트로 DOM이 갈아엎힐 때였다. 매핑이 깨지면 자동 입력이 아무 값도 넣지 못한 채 조용히 멈추는 상황이 생김. 이게 침묵 실패의 전형적인 패턴이라서, 노드 탐색 실패 시점에 즉시 알림을 보내고 중단하는 로직을 의무화했다. 은행 앱 업데이트 직후 첫 송금 건에서 알림이 울리면 매핑 점검이 필요하다는 신호로 받아들이면 된다.

접근성 서비스 화이트리스트는 배포 전에 반드시 좁혀야 한다. 서비스가 구동 중인 상태에서는 전체 앱의 창 전환 이벤트가 다 들어온다. 지정된 패키지 외 이벤트를 즉시 드롭하지 않으면 불필요한 처리가 계속 발생하고, 의도치 않게 다른 앱 화면을 읽을 가능성도 생김. 화이트리스트를 별도 설정 파일로 분리해두면 은행 앱 패키지명이 바뀌었을 때도 대응이 빠르다. 코어 빌드를 건드리지 않고 설정 파일 하나만 업데이트하면 끝이라는 점에서 매핑 테이블과 같은 맥락의 분리임.

댓글 0

첫 댓글 달아줘.