레지덴셜 프록시

레지덴셜 프록시를 이용한 속도 제한 및 요청 조절

대규모 프록시 풀이 있다고 해서 속도 제한에서 자유로워지는 것은 아니며, 단지 제한이 적용되는 지점이 달라질 뿐입니다. 대상별, IP별, 그리고 전체 플릿 단위로 요청 속도를 조절하는 방법을 알아봅니다.

Matt Brown

Matt Brown

2026년 8월 26일 · 6 분 소요

많은 팀을 걸려 넘어지게 하는 그럴듯한 논리가 있다. 속도 제한은 IP 단위로 적용되고, 레지덴셜 풀은 많은 IP를 제공하므로, 더 이상 속도 제한이 적용되지 않는다는 논리다. 앞의 두 명제는 참이지만 결론은 틀렸으며, 그 사이의 간극에 놀랄 만큼 많은 차단된 스크레이퍼들이 놓여 있다.

풀은 제한이 걸리는 지점을 바꿀 뿐, 제한이 존재하는지 여부를 바꾸지는 않는다. 단일 IP 환경과는 다른 축을 따라 여전히 속도를 조절해야 하며, 사람들이 잊어버리는 그 축이 바로 그들을 잡히게 만드는 축이다. 요청이 로테이팅 풀 전체에 분산될 때 스로틀링이 실제로 어떻게 작동하는지 살펴보자.

하나가 아니라 세 개의 제한

풀을 통해 스크레이핑할 때는 최소 세 개의 별도 상한선이 동시에 적용되며, 처리량은 그중 가장 먼저 도달하는 것에 의해 결정된다.

IP당, 대상당. 이것이 고전적인 유형이다. 하나의 주소는 특정 사이트에 일정량 이상의 요청을 보낼 수 없으며, 이를 초과하면 해당 사이트가 스로틀링하거나 차단한다. 로테이션은 이 제한 아래에 머물도록 해주는 수단이며, 부하 분산에서 설명한 것처럼 풀 전체에 부하를 분산하는 것의 핵심이다.

대상당, 총합. 이것이 사람들을 놀라게 하는 유형이다. 사이트는 주소별로만 카운트하지 않는다. 자체 전체 수신 부하도 파악하며, 특히 요청들이 타이밍, 경로, 또는 행동 패턴을 공유할 경우 여러 주소에 걸친 조직적인 패턴을 인식할 수 있다. 50개의 IP가 각각 초당 2개의 요청을 보낸다면 이는 하나의 원본 서버에 초당 100개의 요청이 도달하는 것이며, 아무리 주소를 로테이션해도 이것이 일반적인 유기적 트래픽처럼 보이지는 않는다. 안티봇 시스템은 점점 더 이 수준에서 판단하고 있다.

자신의 용량. 실제로 지속 가능한 동시성이다. 열려 있는 연결, 워커 스레드, 메모리, 그리고 레지덴셜 지연 시간이 직접 연결보다 높다는 사실 때문에 고정된 스레드 수는 예상보다 적은 초당 요청 수를 만들어낸다.

실질적인 결론은 스로틀이 전역이 아니라 대상별로 표현되어야 한다는 것이다. 단일 전역 속도 제한은 더 많은 처리가 가능한 50개 사이트에서 자원을 굶기거나, 그럴 여유가 없는 사이트를 두드려대게 만든다.

자신의 목표가 아니라 대상에서 시작하라

스로틀링 코드를 작성하기 전에, 대상이 실제로 허용하는 범위를 파악해야 한다. 추측은 불필요하게 느린 작업이나 차단된 작업 중 하나를 만들어내기 때문이다.

사이트가 주는 신호를 읽어라. 429는 너무 빠르다는 명시적인 표시다. Retry-After 헤더는 사이트가 얼마나 기다려야 하는지 정확히 알려주는 것이며, 이를 따르는 것이 옳을 뿐 아니라 시행착오로 답을 알아내는 것보다 훨씬 저렴하다. 일부 API는 문서에 제한을 명시하거나 남은 할당량 헤더를 반환한다. 그리고 사이트에 크롤 지연 지시자가 있는 robots.txt가 있다면, 이는 존중할 가치가 있는 명시된 선호다.

아무것도 공개되어 있지 않은 경우에는 경험적으로 보정하라. 보수적으로 시작해 점진적으로 늘리고, 상태 코드가 아니라 검증된 성공률을 지켜봐야 한다. 부하를 받는 사이트는 거부하기 전에 성능이 저하되는 경우가 많고, 차단되거나 가짜인 콘텐츠 탐지하기에서 다룬 것처럼 상태 코드 200을 반환하는 챌린지 페이지는 단순한 카운터에는 성공처럼 보이기 때문이다. 성공률이 떨어지기 시작하는 지점이 실제 상한선이며, 그 지점에서가 아니라 그 아래에서 운영해야 한다.

스로틀 구현하기

대상별 토큰 버킷은 표준 도구이며 처음부터 작성하기에도 충분히 간단하다. 토큰은 선택한 속도로 다시 채워지고, 각 요청은 하나를 소비하며, 버킷이 비어 있으면 요청이 대기한다. 이는 안정적인 평균과 제어된 버스트 허용치를 제공하며, N초마다 하나의 요청이라는 경직된 지연 방식보다 실제 트래픽이 작동하는 방식과 더 잘 맞는다.

import time, threading

class TargetLimiter:
    """Token bucket, one instance per target host."""
    def __init__(self, rate_per_sec, burst=5):
        self.rate, self.capacity = rate_per_sec, burst
        self.tokens, self.updated = burst, time.monotonic()
        self.lock = threading.Lock()

    def acquire(self):
        while True:
            with self.lock:
                now = time.monotonic()
                self.tokens = min(self.capacity,
                                  self.tokens + (now - self.updated) * self.rate)
                self.updated = now
                if self.tokens >= 1:
                    self.tokens -= 1
                    return
                wait = (1 - self.tokens) / self.rate
            time.sleep(wait)               # sleep outside the lock

LIMITS = {                                  # per target, tuned per target
    "shop.example.com":  TargetLimiter(2.0),
    "search.example.com": TargetLimiter(0.5),
}

def fetch(url, host, proxies):
    LIMITS[host].acquire()
    return requests.get(url, proxies=proxies, timeout=20)

프로덕션에서는 두 가지 개선 사항이 중요하다. 요청이 정확한 간격으로 발생하지 않도록 지터를 추가하라. 완벽하게 규칙적인 간격 자체가 기계임을 나타내는 신호이며, 워커들을 파도처럼 동기화시키기도 한다. 그리고 제한기를 프로세스별이 아니라 워커 전체가 공유하도록 만들어라. 그렇지 않으면 초당 2개의 요청을 정중하게 수행하는 10개의 프로세스가 합쳐서 초당 20개의 요청을 수행하게 된다. 분산 환경에서는 이것이 Redis 같은 곳의 공유 카운터를 의미하며, 이는 쿠버네티스에서 스크레이핑 확장하기가 해결해야 하는 것과 동일한 문제다.

동시성은 또 다른 절반이다

속도와 동시성은 서로 다른 다이얼이며 둘 다 제한이 필요하다. 속도는 요청을 시작하는 빈도를 제어하고, 동시성은 동시에 진행 중인 요청의 수를 제어한다. 적당한 속도 제한이 있지만 동시성 제한이 없는 작업은 특정 원본 서버가 느려지는 순간 여전히 그 서버에 수백 개의 동시 연결을 열게 된다. 느린 응답이 요청을 쌓이게 만들기 때문이다.

세마포어로 대상별 동시성을 제한하고, 자신의 머신이 열 수 있는 양이 아니라 대상이 허용하는 양에 맞춰 크기를 정하라. 프록시 측에서는 동시 연결이 일반적으로 제약 조건이 아니기 때문에, 대상의 허용치가 제약 조건이라는 사실을 잊기 쉽다. 이것이 시사하는 필요한 주소 수에 대한 분산 질문은 실제로 필요한 프록시 IP 수에서 다뤄진다.

적응형 스로틀링이 고정된 숫자보다 낫다

고정된 속도는 결국 틀리게 되는 추측이다. 사이트는 허용치를 바꾸거나, 부하 상태에서 느려지거나, 피크 시간대에 제한을 강화한다. 더 견고한 패턴은 관찰한 것을 기반으로 조정하는 것이며, 가산 증가와 승산 감소 방식을 따르는 것이다. 상태가 좋을 때는 속도를 천천히 올리고, 부하 징후가 처음 나타나면 급격하게 줄인다.

하나의 상태 코드가 아니라 복합 신호에 대해 감소를 트리거하라. 429, 챌린지 페이지 비율 상승, 지연 시간 증가, 또는 검증된 성공률 하락 등이다. 그런 다음 이전 속도로 바로 돌아가지 말고 점진적으로 회복하라. 차단 직후 즉시 전속력으로 복귀하는 것 자체가 인식 가능한 패턴이기 때문이다.

class AdaptiveRate:
    def __init__(self, start=2.0, floor=0.2, ceiling=10.0):
        self.rate, self.floor, self.ceiling = start, floor, ceiling

    def ok(self):                       # healthy response
        self.rate = min(self.ceiling, self.rate * 1.02)   # creep up

    def strained(self):                 # 429, challenge, timeout, latency spike
        self.rate = max(self.floor, self.rate * 0.5)      # back off hard

스로틀링이 로테이션 및 재시도와 만나는 지점

세 가지 상호작용은 명시적으로 짚어둘 가치가 있다. 이를 잘못 처리하면 스로틀 자체가 무력화되기 때문이다.

속도 신호에 로테이션으로 대응하지 마라. 429는 속도를 줄이라는 뜻이다. 같은 속도를 유지하기 위해 새 주소로 바꾸는 것은 하나의 스로틀링된 주소를 풀 전체를 태워버리는 행동으로 만들며, 이것이 바로 트래픽을 단순히 열정적인 것이 아니라 회피적으로 보이게 만드는 원인이다. 신호가 속도에 관한 것이면 기다리고, 신호가 주소에 관한 것이면 로테이션하라.

재시도는 제한기를 따라야 한다. 재시도는 또 다른 요청이며 다른 요청과 마찬가지로 토큰을 획득해야 한다. 그렇지 않으면 오류 경로가 조용히 애써 만든 속도 조절을 우회하게 된다. 재시도 폭주는 스로틀링된 작업이 차단된 작업으로 바뀌는 가장 흔한 방식이며, 이는 재시도와 백오프에서 다루는 주제다.

스티키 세션은 부하를 집중시킨다. 다단계 흐름을 위해 하나의 주소를 유지하는 것은 그 주소가 전체 시퀀스를 담당하게 됨을 의미하므로, 스티키 세션 내에서는 IP당 속도 조절이 부하가 자연스럽게 분산되는 로테이팅 작업에서보다 더 중요하다.

예의는 자기 이익이다

이 모든 것을 준법 의무 정도로 읽기 쉽지만, 유인은 같은 방향을 가리킨다. 속도 조절은 의존하는 주소들의 평판을 보존하고, 성공률을 높게 유지해 챌린지 페이지에 대역폭 비용을 지불하지 않게 해주며, 공격적인 수집이 더 엄격한 방어를 유발해 다음 분기에는 자신을 포함한 모두에게 대상을 더 어렵게 만드는 확대 순환을 피하게 해준다. 사이트가 명시한 제한, robots 지시자, 그리고 이용약관을 존중하는 것은 옳은 일이면서 동시에 더 저렴한 일이다.

결론

풀은 속도 제한을 제거하지 않는다. 제한의 위치를 옮길 뿐이다. 전역이 아니라 대상별로 스로틀을 적용하라. 하나의 숫자는 접촉하는 모든 사이트에 대해 틀린 숫자이기 때문이다. 각 대상의 실제 허용치를 그 자체의 신호로부터 파악하고, Retry-After429를 장애물이 아니라 지시로 존중하며, 상태 코드가 아니라 검증된 성공률을 기준으로 보정하라. 공유 토큰 버킷과 지터로 구현하고, 동시성을 속도와 별도로 제한하며, 스로틀이 빠르게 저하되고 천천히 회복되도록 적응형으로 만들어라. 그런 다음 상호작용을 명확히 유지하라. 속도 조절 신호에 절대 주소 로테이션으로 대응하지 말고, 재시도는 항상 제한기를 거치게 하며, 스티키 세션 안에서는 더 강하게 속도를 조절하라. 잘 수행된 스로틀링은 풀이 실제 용량으로 작동하게 해주며, 풀을 소진시키지 않게 해준다.

풀 자체가 작업할 여지를 제공한다. 레지덴셜 프록시는 국가 및 도시 타겟팅이 가능한 실제 가정용 등급 주소 다수에 걸쳐 적절히 속도가 조절된 작업을 분산시키며, GB당 가격 책정은 속도를 잘 조절해 필요한 만큼만 가져오는 작업이 마구잡이로 두드리고 재시도하는 작업보다 비용이 적게 든다는 것을 의미한다.

시작할 준비가 되셨나요?

205M개 이상의 IP, 195개 이상의 국가를 지원하는 Shifter의 레지덴셜 프록시를 $0.75/GB부터 이용해보세요.

시작하기