입금 크롤링을 HTTP 직접 호출로 교체해 응답 10배 빠르게
목차
입금 내역 확인용 유틸이 헤드리스 브라우저로 은행 사이트를 띄워서 DOM을 긁는 구조였다. 처음 만들 때는 빠르게 돌아가면 됐고, 어차피 자주 바꿀 것도 아니라고 생각했는데 - 이게 전형적인 방심이었다.
헤드리스 브라우저는 실제 크롬 엔진을 올려서 돌리기 때문에 컨테이너 메모리가 1GB 가까이 잡아먹힌다. 조회 한 번 하려고 렌더링 엔진 전체를 올리는 셈인데, 응답 대기까지 합치면 8-12초가 걸렸다. 그것도 운이 좋을 때. 셀렉터 하나가 어긋나면 그냥 전체가 멈춰버렸다. 새벽에 알람 받고 일어난 게 한두 번이 아니었고, 어느 순간부터는 알람을 끄면서 "다음에 갈아엎자"고 생각하는 게 루틴이 됐다. 결국 갈아엎었다.
패킷부터 까봤다
진짜 동작을 패킷 캡처로 뜯어보면 거의 항상 단순하다. 이번도 마찬가지였다. 로그인 폼 POST, 세션 쿠키 수신, 거래내역 엔드포인트 GET - 이게 전부였다. 렌더링 엔진을 올릴 이유가 없었다.
흐름을 정리하면 이렇다:
- 폼 파라미터 그대로 POST해서 세션 쿠키를 받는다
- 응답 본문에서 인증 토큰을 파싱해 다음 요청 헤더에 싣는다
- 거래내역 엔드포인트를 때려서 응답을 도메인 객체로 매핑한다
- 직접 호출이 깨질 때만 헤드리스 경로로 폴백한다
폴백을 남겨둔 건 의도적인 결정이었다. 처음부터 헤드리스를 완전히 걷어내버리면 상대 사이트가 흐름을 바꿨을 때 아무 안전망이 없다. 한 달 정도 카운트를 지켜보고 계속 0이면 그때 조용히 지울 생각이다.
실제 코드는 대략 이런 구조로 짰다:
session = requests.Session()
# 1. 로그인 - Content-Type 헤더 믿지 말 것
resp = session.post(LOGIN_URL, data=form_params)
resp.encoding = "euc-kr"
# 2. hidden input에서 보안 토큰 추출
token = parse_hidden_token(resp.text)
# 3. 거래내역 조회
result = session.get(
TXN_URL,
headers={"X-Auth-Token": token},
cookies=session.cookies,
)
result.encoding = "euc-kr"
txns = parse_transactions(result.text)
# 4. 신뢰 단언 - 200이라고 성공이 아님
assert len(txns) > 0, "거래내역 비어있음 - 파싱 실패 의심"
assert txns[0].amount > 0, "금액 파싱 실패"
코드만 보면 별 게 없다. 문제는 이 단순한 흐름에 이르기까지 꽤 헤맸다는 것.
삽질 두 가지
EUC-KR 인코딩. 응답이 EUC-KR에 iframe 안에 form이 중첩된 전형적인 레거시 구조였다. Content-Type 헤더를 믿고 UTF-8로 파싱했더니 한글이 전부 깨졌다. requests는 헤더에 charset이 명시되지 않으면 ISO-8859-1을 기본으로 쓰는데, 이게 EUC-KR과 달라서 이중으로 꼬였다. HTTP 클라이언트 단에서 resp.encoding = "euc-kr"로 강제 지정하고 나서야 정상 파싱이 됐다. 오래된 금융권 사이트는 Content-Type 헤더를 절대 믿으면 안 된다는 교훈을 비싸게 배웠다.
hidden 보안 토큰. 일반 세션 쿠키 외에 별도 보안 토큰을 hidden input으로 넘기는 구조가 있었다. 이걸 빠뜨리면 거래내역 조회 응답이 HTTP 200에 빈 배열로 돌아온다. 에러를 뱉지 않는다. 200 OK다. 첫날 거의 두 시간을 이것 때문에 날렸다. "응답 200 = 성공"이라는 전제를 버리고 단언을 박아서 해결했는데, 그 이후에도 이 단언이 여러 번 조용한 실패를 미리 잡아줬다.
응답 200이라고 성공이 아님 - 검증 체크리스트
- 배열 size > 0
- 첫 행 금액 > 0
- 거래일자 파싱 가능
상대 사이트 개편으로 응답 구조가 살짝 바뀌었을 때도, 빈 배열을 조용히 넘기는 대신 즉시 알람이 떴다. 깨질 때 시끄럽게 깨지는 게 조용히 빈 배열로 깨지는 것보다 운영 입장에서 백 배 낫다.
효과와 남은 과제
| 구분 | 변경 전 | 변경 후 |
|---|---|---|
| 평균 응답 | 8-12초 | 0.6-1.2초 |
| 컨테이너 메모리 | 약 1GB | 약 250MB |
| 외부 의존 | 헤드리스 + 드라이버 바이너리 | HTTP 클라이언트만 |
| 새벽 호출 빈도 | 주 2-3회 | 0 (한 달째) |
숫자보다 체감이 명확했다. 새벽 알람이 한 달째 없다. 배포 파이프라인도 단순해졌다. 헤드리스 브라우저를 올리려면 크롬 바이너리와 드라이버를 이미지에 포함시켜야 했는데, 그게 사라지니까 이미지 크기도 같이 줄었고 빌드 시간도 짧아졌다.
제일 크게 남은 교훈은 "브라우저 자동화는 마지막 카드"라는 것. 트래픽을 직접 까보면 의외로 평범한 HTTP 호출인 경우가 많다. 사이트가 동적으로 렌더링되는 것처럼 보여도 실제 데이터는 XHR이나 폼 POST 몇 번으로 끝나는 경우가 대부분이다. 헤드리스 도구가 강력하긴 한데, 운영 비용도 정직하게 따라온다. 메모리, 의존성, 안정성 전부.
반대로 직접 HTTP 호출로 가면 상대 사이트 개편 한 번에 직격탄을 맞는 리스크가 있다. 그래서 응답 구조 변화를 감지할 헬스체크 한 줄은 반드시 박아둬야 한다. 파싱 실패를 침묵으로 처리하지 않는 것. 단언이든 예외든 뭔가 시끄럽게 터져야 운영자가 인지할 수 있다.
폴백으로 남겨둔 헤드리스 경로는 한 달 더 카운트가 0이면 그때 걷어낼 예정이다. 코드 한 줄 지우는 것도 나름 작은 회고감이 된다.
댓글 0
첫 댓글 달아줘.