가장 흔한 스크래핑 설정은 설계 시점에 프록시 구성을 한 번 선택하고 그대로 유지한다. 이 사이트에는 도시 타겟팅이 적용된 레지덴셜, 저 사이트에는 ISP 프록시, 되는 곳에는 데이터센터. 이런 선택은 보통 하루 오후 동안 몇 가지 옵션을 시도해보고 정해지며, 무언가 고장 나기 전까지는 거의 다시 검토되지 않는다.
이것은 불확실성 아래에서 내려지는 결정이며, 하루에도 수천 번 반복되고, 요청마다 피드백이 뒤따른다. 이는 통계학자들이 다중 슬롯머신 문제(multi-armed bandit)라고 부르는 문제의 전형적인 형태이며, 이를 위한 잘 알려진 알고리즘들이 존재한다. 이 가이드는 그 틀을 설명하고, 짧고 테스트된 구현을 제시하며, 중간에 최적의 선택지가 바뀌는 시뮬레이션에서 어떤 결과가 나왔는지 보여준다.
핵심 요약
- 어떤 타겟에 사용할 수 있는 각 프록시 구성은 하나의 “암(arm)“이고, 각 요청은 하나의 “풀(pull)“이며, 좋은 응답은 하나의 “보상(reward)“이다. 목표는 어떤 비용을 치르더라도 최고의 성공률을 내는 것이 아니라, 좋은 응답 하나당 가장 낮은 비용을 내는 것이다.
- 톰슨 샘플링(Thompson sampling)은 가장 잘 알려진 옵션을 사용하는 것과 다른 옵션들을 시험해보는 것 사이의 트레이드오프를 처리하며, 몇 줄의 코드만으로 가능하고 별도의 튜닝 스케줄도 필요 없다.
- 타겟은 변하므로 오래된 증거는 점차 희미해져야 한다. 할인 계수(discount factor)는 선택기에게 대략 최근 1,000개 결과에 대한 기억을 부여한다.
- 우리의 시뮬레이션에서, 할인된 톰슨 샘플링은 실제 비율을 미리 알고 있는 전략 대비 2% 이내의 차이를 보였으며, 고정된 “안전한” 선택은 좋은 응답당 25% 더 많은 비용이 들었고, 초기 승자를 계속 고수하는 전략은 57% 더 많은 비용이 들었다.
- 진정으로 좋은 응답만을 성공으로 집계하고, 민감한 타겟에 대해 탐색(exploration)이 얼마나 이루어지는지 제한하라.
틀 잡기
알 수 없는 확률로 보상을 지급하는 일렬의 슬롯머신을 떠올려보자. 한 번 당길 때마다 하나의 기계에 대해 무언가를 배우게 되며, 나쁜 기계를 배우는 데 쓴 한 번의 당김은 좋은 기계에 쓸 수 없었던 한 번이다. 알고 있는 것을 활용(exploiting)하는 것과 모르는 것을 탐색(exploring)하는 것 사이의 이 긴장이 바로 다중 슬롯머신 문제다.
프록시 선택에 있어서 이 대응 관계는 직접적이다:
| 밴딧 용어 | 스크래핑에서의 의미 |
|---|---|
| 암(Arm) | 하나의 타겟에 대한 프록시 구성: 프록시 유형, 타겟팅, 세션 설정 |
| 풀(Pull) | 요청 1회 |
| 보상(Reward) | 원하는 데이터를 포함한 응답 |
| 풀의 비용 | 그 요청에 소요된 대역폭, 재시도, 시간 |
| 비정상성(Non-stationarity) | 타겟이 방어 체계를 바꾸거나, 풀(pool)의 평판이 변하는 것 |
두 가지 특징이 스크래핑 버전을 교과서적 버전보다 더 어렵게 만든다. 암마다 비용이 다르므로, 최선의 암은 성공률이 가장 높은 암이 아니라 성공당 비용이 가장 낮은 암이다. 그리고 비율은 변동한다. 오늘 작동하는 구성이 내일은 차단될 수 있는데, 이는 서브넷 전염의 배경이 되는 패턴이자 타겟 건강 점수를 구축해야 하는 이유이기도 하다.
선택기
톰슨 샘플링은 각 암의 성공률에 대한 확률 분포를 유지하며, 이는 해당 암의 성공과 실패로부터 만들어진 베타(Beta) 분포다. 각 요청 전에 모든 암에 대해 하나의 그럴듯한 비율을 뽑고, 그 뽑힌 값들 아래에서 성공당 비용이 가장 낮아 보이는 암을 선택한다. 증거가 적은 암은 분포가 넓어서, 가끔 높은 비율을 뽑아 시도되기도 한다. 증거가 많은 암은 실제 비율에 가까운 값을 뽑는다. 탐색은 증거가 쌓임에 따라 저절로 줄어든다.
아래 버전은 두 가지를 추가한다: 시도당 비용, 그리고 오래된 증거를 사전분포 쪽으로 감쇠시켜 선택기가 변화를 알아챌 수 있도록 하는 할인(discount).
import random
class ProxySelector:
"""Pick a proxy configuration per request with Thompson sampling, minimising cost per successful response.
Each configuration keeps a Beta(successes, failures) belief about its success rate. A discount below 1
lets old outcomes fade, so the selector notices when a configuration that used to work starts failing.
"""
def __init__(self, costs, discount=0.999, prior=(1.0, 1.0), rng=None):
self.costs = dict(costs) # configuration -> cost per attempt
self.discount = discount
self.prior = prior
self.wins = {arm: prior[0] for arm in self.costs}
self.losses = {arm: prior[1] for arm in self.costs}
self.rng = rng or random.Random()
def choose(self):
"""Sample a plausible success rate for each configuration and take the cheapest per expected success."""
def sampled_cost_per_success(arm):
rate = self.rng.betavariate(self.wins[arm], self.losses[arm])
return self.costs[arm] / max(rate, 1e-9)
return min(self.costs, key=sampled_cost_per_success)
def update(self, arm, success):
"""Record one outcome. Every belief decays towards the prior first, so recent results count most."""
for a in self.costs:
self.wins[a] = self.prior[0] + self.discount * (self.wins[a] - self.prior[0])
self.losses[a] = self.prior[1] + self.discount * (self.losses[a] - self.prior[1])
if success:
self.wins[arm] += 1
else:
self.losses[arm] += 1
사용할 때는 각 요청 전에 choose()를 호출하고, 선택된 구성을 통해 요청을 보낸 다음, 응답이 좋았는지 여부와 함께 update()를 호출하면 된다. 0.999의 할인 값은 약 1,000개 결과에 대한 유효 기억을 부여하며, 0.995는 약 200개에 대한 기억을 부여한다.
시뮬레이션
답이 바뀔 때 선택기가 어떻게 동작하는지 보기 위해, 우리는 네 가지 구성을 가진 하나의 타겟을 시뮬레이션했다. 아래의 성공률과 비용은 어떤 제공업체나 네트워크의 측정값이 아니라, 트레이드오프를 눈에 보이도록 선택한 가정값이다:
| 구성 | 가정된 성공률 | 가정된 시도당 비용 | 성공당 비용 |
|---|---|---|---|
| 데이터센터 | 25% | 0.5 | 2.00 |
| ISP | 80%, 요청 5,000회 이후 30%로 하락 | 0.9 | 1.13, 이후 3.00 |
| 레지덴셜, 국가 타겟팅 | 92% | 1.3 | 1.41 |
| 레지덴셜, 도시 타겟팅 | 95% | 1.6 | 1.68 |
비용은 대역폭과 시도의 오버헤드를 포함하는 상대적 단위다. 처음 5,000회 요청 동안은 ISP가 좋은 응답을 얻는 가장 저렴한 방법이었으나, 이후 타겟이 이를 차단하기 시작하면서 국가 타겟팅을 적용한 레지덴셜이 최선의 선택이 되었다. 각 전략은 20,000회의 요청을 수행했으며, 서로 다른 난수 시드로 각 전략을 100회 반복했다.
| 전략 | 좋은 응답당 비용 | 완벽한 사후 지식 대비 | 좋은 응답 수 | 변화 이후 적응까지의 요청 수 |
|---|---|---|---|---|
| 완벽한 사후 지식 (실제 비율을 알고 있음) | 1.348 | 기준선 | 17,806 | 0 |
| 톰슨 샘플링, 할인 0.999 | 1.375 | +2.0% | 16,694 | 약 800 |
| 톰슨 샘플링, 할인 0.995 | 1.389 | +3.0% | 15,704 | 약 1,075 |
| 톰슨 샘플링, 할인 없음 | 1.428 | +5.9% | 15,933 | 약 3,500 |
| 엡실론-그리디, 탐색 10% | 1.441 | +6.9% | 15,848 | 약 2,800 |
| 톰슨 샘플링, 할인 0.98 | 1.443 | +7.0% | 13,834 | 결국 안정되지 않음 |
| 항상 레지덴셜, 도시 타겟팅 | 1.684 | +24.9% | 19,000 | 해당 없음 |
| 네 가지 모두 라운드 로빈 | 1.689 | +25.3% | 12,731 | 해당 없음 |
| 항상 ISP, 초기 승자 | 2.118 | +57.1% | 8,501 | 해당 없음 |
수치는 100회 실행의 평균이며, 모든 전략에서 실행의 중간 90%는 평균값의 2.5% 미만의 범위에 들었다. “적응까지의 요청 수”는 변화 이후 200회 요청 윈도우의 90%가 새로운 최선의 구성으로 몰릴 때까지 걸린 요청 수의 중앙값이다.
결과가 말해주는 것
고정된 선택은 양방향 모두에서 비용이 크다. 가장 신뢰할 수 있는 구성을 항상 사용하는 전략은 가장 많은 좋은 응답을 냈지만, 각 응답에 25% 더 많은 비용을 지불했다. 초기에 승리한 구성을 항상 사용하는 전략은 타겟이 바뀐 뒤에는 모든 전략 중 가장 나빴으며, 기준선 대비 57% 초과였다.
잊는 과정 없이는 학습만으로 충분하지 않다. 할인되지 않은 톰슨 샘플링은 변화 이후 옛 승자에 대해 쌓아온 증거를 더 이상 신뢰하지 않게 되기까지 약 3,500회의 요청이 필요했다. 0.999의 할인은 이를 약 800회로 줄였다.
너무 빨리 잊는 것도 그 자체로 문제다. 할인 값이 0.98일 때, 선택기는 약 50개의 결과만 기억했고, 성과가 나쁜 옵션들을 계속 재시험했으며, 결국 안정되지 않았다. 유용한 범위는 트래픽에 따라 달라진다. 기억은 구성들을 구별하기에 충분한 만큼의 요청을 포괄해야 하며, 그 이상은 필요 없다.
목표가 답을 결정한다. 이 밴딧은 좋은 응답당 비용을 최소화했다. 만약 중요한 것이 비용과 무관하게 최대한 많은 데이터를 얻는 것이거나 마감 기한이라면, 보상 함수가 그것을 반영해야 한다. 그렇지 않으면 선택기는 잘못된 목표를 매우 효율적으로 최적화하게 된다.
실제 트래픽에 적용하기
- 성공을 엄격하게 정의하라. 차단 페이지나 빈 결과를 포함한 HTTP 200 응답은 실패다. 무응답 실패율에 사용하는 것과 동일한 검증을 선택기에 입력하라. 그렇지 않으면 선택기는 조용히 실패하는 구성을 선호하도록 학습하게 된다.
- 타겟마다 별도의 선택기를 운영하라. 성공률은 사이트마다 다르므로, 공유된 선택기는 바로 학습하기를 원하는 그 차이를 평균으로 뭉개버린다.
- 민감한 타겟에 대해서는 탐색을 제한하라. 성과가 나쁜 구성을 통한 탐색적 요청 하나하나는 사이트가 당신에게 불리하게 집계할 수 있는 요청이다. 선택기가 그것을 다시 발견하도록 두기보다, 명백히 부적합한 구성은 제거하라.
- 세션을 일관되게 유지하라. 로그인이나 장바구니처럼 상태에 의존하는 모든 것에 대해서는, 고정 세션과 로테이팅 세션에서 설명하듯이 요청 단위가 아니라 세션 단위로 구성을 선택하라.
- 실제 비용을 사용하라. 대역폭 기준 과금 프록시에서는 시도당 비용이 페이지 무게와 재시도에 따라 달라진다. 클린 레코드당 비용의 배경이 되는 수치들이 올바른 입력값이다.
- 로드 밸런싱의 하한선을 유지하라. 밴딧은 어떤 구성을 사용할지를 결정하는 것이지, 그 구성 내에서 요청을 어떻게 분산할지를 결정하는 것이 아니다. 그것은 여전히 로테이션, 동시성, 재시도 아키텍처의 몫이다.
결론
프록시 구성을 한 번 선택하고 그대로 유지하는 것은 당신의 비용도 타겟도 변하지 않을 것이라는 내기다. 각 요청을 작은 실험으로 취급하고, 톰슨 샘플링과 합리적인 기억을 적용하면, 그 내기는 계속 업데이트되는 하나의 측정으로 바뀐다.
우리의 시뮬레이션에서는 이것이 답을 미리 알고 있는 것 대비 2% 이내의 차이를 냈고, 최선의 옵션이 바뀌었을 때 약 800회의 요청 안에 적응했으며, 고정 전략들의 25%에서 57%에 달하는 프리미엄을 피했다. 비율과 비용은 가정값이었으며, 코드는 당신 자신의 데이터에 대해 실행해보기에 충분히 짧다.
출처 및 참고자료
- Daniel Russo 및 동료들, A Tutorial on Thompson Sampling, 2017.
- Shifter가 2026년 10월 2일에 위 코드를 사용하여 실행한 시뮬레이션. 성공률과 비용은 어떤 네트워크의 측정값이 아니라 가정값이다.