봇 유입을 버리지 말고 표시만 하기: 클라우드 IP 대역 판별
사이트에 유입 분석을 붙였다. 페이지를 볼 때마다 리퍼러, 검색어, 기기, 브라우저, 대략적인 지역을 기록하고 관리자 화면에서 날짜별로 보는 기능이다. 붙이고 하루가 지나자 이상한 방문이 눈에 띄었다. 해상도가 1920x1080인 'iOS 사파리', 리퍼러가 옛 주소인데 위치가 아일랜드 더블린인 리눅스 크롬. 사람이라고 보기 어려운 방문이었다.
User-Agent만으로는 안 된다
봇 판별은 보통 User-Agent에서 시작한다. Googlebot, HeadlessChrome 같은 문자열을 걸러 주는 라이브러리(isbot)를 이미 쓰고 있었다. 하지만 위의 방문들은 평범한 브라우저 UA를 달고 있었다. UA는 보내는 쪽이 마음대로 쓰는 문자열이라, 숨기려고 마음먹은 봇은 UA로 못 잡는다.
숨기기 어려운 건 어디서 접속했는가다. 사람은 집이나 회사, 통신사망에서 들어온다. AWS, GCP, Azure 같은 데이터센터 IP에서 브라우저로 들어오는 건 대부분 크롤러, 모니터링, 미리보기 생성기, 스캐너다.
클라우드 업체는 IP 대역을 공개한다
다행히 큰 클라우드 업체들은 자기 IP 대역을 기계가 읽을 수 있는 형태로 공개한다.
| 업체 | 공개 방식 |
| AWS | ip-ranges.json (리전·서비스별) |
| GCP | cloud.json |
| Azure | Service Tags 주간 JSON (파일 주소가 매주 바뀌어서 안내 페이지에서 링크를 찾아야 함) |
| Oracle, DigitalOcean | JSON / CSV |
하루 한 번 이 목록을 받아 DB에 저장하고, 방문 기록마다 IP가 어느 업체 대역에 속하는지 표시하기로 했다. 한 업체 목록 받기에 실패하면 그 업체는 예전 목록을 그대로 쓴다. 하나가 실패했다고 판별 전체가 비면 안 되니까.
수만 개 대역에서 빠르게 찾기
대역은 CIDR(52.95.0.0/16) 수만 개다. 방문마다 전부 훑으면 O(n)이다. 대신 이렇게 했다.
- CIDR을 시작·끝 정수 구간으로 바꾼다. IPv4는 32비트 정수, IPv6는 128비트라 BigInt로.
- 시작 주소로 정렬하고, 겹치거나 맞닿은 구간은 합친다. 업체별로 하나의 정렬된 구간 배열이 된다.
- 조회할 IP도 정수로 바꿔서, '시작 ≤ IP'인 마지막 구간을 이진 탐색으로 찾고 그 끝보다 작거나 같은지 본다.
병합해 두면 구간끼리 겹치지 않으니 '시작이 IP 이하인 마지막 구간' 하나만 보면 된다. 병합하지 않으면 그 앞 구간들도 확인해야 한다.
버리지 말고 표시만
처음엔 클라우드 IP 방문을 아예 저장하지 않을까 생각했다. 하지만 그러지 않았다.
- 판별이 틀릴 수 있다. 회사 VPN이나 클라우드 PC로 들어오는 진짜 사람도 있다.
- 목록을 늦게 받으면 그 사이 방문은 판별 전 상태다. 버렸다면 되돌릴 수 없다.
- 봇 유입 자체도 정보다. 어떤 크롤러가 얼마나 오는지, 옛 주소로 아직 누가 들어오는지.
그래서 기록에는 cloud 필드(업체 이름, 아니면 빈 문자열)만 붙이고, 날짜별 집계는 전체와 '클라우드 뺀 것' 두 벌을 만든다. 관리자 화면의 '클라우드 IP 빼기'는 기본으로 켜 두되, 끄면 전부 볼 수 있다. 목록이 아직 없을 때 들어온 기록은 필드를 아예 비워 두고, 매시간 작업이 목록이 생긴 뒤에 채우고 그 날짜 집계를 다시 계산한다. '아니다(빈 문자열)'와 '아직 모른다(필드 없음)'를 구분해 둔 덕분이다.
개인정보와 같이 설계하기
IP로 판별하려면 IP를 저장해야 한다. 원본 IP는 30일만 두고 지우고, 방문 기록은 180일 뒤 TTL 인덱스로 자동 삭제, 그 뒤에는 날짜별 집계만 남게 했다. 순방문자 수는 IP를 그대로 쓰지 않고 날짜·IP·UA를 비밀값으로 HMAC한 값으로 센다. 날짜가 들어가서 하루가 지나면 같은 사람도 다른 값이 된다. 이 내용은 개인정보 처리방침에 그대로 적었다.
배운 점
- 위조할 수 있는 신호(UA)보다 위조하기 어려운 신호(접속 네트워크)를 먼저 본다.
- 범위 조회는 '정렬 + 병합 + 이진 탐색'이면 대부분 충분하다. 특수한 자료구조(인터벌 트리, 트라이)는 그다음이다.
- 분류 결과는 원본을 버리는 필터가 아니라 붙여 두는 라벨로. 판별 규칙이 바뀌어도 다시 계산할 수 있다.
- '거짓'과 '모름'을 다른 값으로 저장하면 나중에 채워 넣을 수 있다.
- 외부 목록에 기대는 기능은 받기 실패를 기본값으로 생각한다. 이번에도 개발 환경에서는 그 주소들이 막혀 있어서, 실제 다운로드는 운영에서 처음 확인했고 관리자 화면에 업체별 대역 수와 실패 여부를 띄워 두었다.