← 블로그로 돌아가기

Rust reqwest에서 프록시 설정: SOCKS5, 클라이언트 풀 및 IP 회전을 통한 파서 최적화

Rust reqwest에서 SOCKS5 프록시를 연결하고, 클라이언트 풀을 구축하며, 차단 없이 안정적인 파싱을 위한 IP 회전을 구현하는 방법을 설명합니다.

📅2026년 10월 18일

실제 사이트에서 5분 이상 작동하는 모든 파서는 언젠가는 IP 차단에 부딪히게 됩니다. Rust는 reqwest 라이브러리와 함께 뛰어난 성능을 제공하지만, 기본적으로 프록시 회전 문제를 해결하지는 않으므로 직접 구현해야 합니다. 이 기사에서는 SOCKS5 및 HTTP(S) 프록시를 연결하고 클라이언트 풀을 구성하며 IP 회전을 조직하여, 파서가 첫 번째 429 및 403 응답 후에 중단되지 않도록 하는 방법을 설명합니다.

파서에 프록시가 필요한 이유와 그 부재의 위험성

마켓플레이스, 가격 집계기 및 소셜 미디어와 같은 사이트는 하나의 IP에서 비정상적인 활동을 추적합니다: 초당 수십 개의 요청, 동일한 User-Agent, 요청 간 쿠키 없음. 의심스러운 행동의 수가 임계값을 초과하면, IP는 차단 목록에 올라갑니다 — 일시적이거나 영구적입니다. 파서에게 이는 IP가 수동으로 변경될 때까지 데이터 수집이 완전히 중단됨을 의미합니다.

프록시는 요청을 수십 또는 수백 개의 IP 주소 간에 분산시켜 이 문제를 해결합니다. 이때 단순히 "프록시를 대입하는 것"이 아니라 연결 풀과 회전을 잘 조직하는 것이 중요합니다. 그렇지 않으면 reqwest 자체의 한도(하나의 클라이언트 = 하나의 연결 풀)에 부딪히거나 동일한 프록시에서 너무 자주 요청을 보내게 됩니다. 이후 두 가지 문제를 차례로 살펴보겠습니다: 먼저 기본 연결, 그 다음 풀의 아키텍처입니다.

프로덕션 파서에게는 IP 회전뿐만 아니라 주소의 질도 중요합니다. Wildberries, Ozon 또는 Avito를 파싱하는 경우, 데이터 센터 IP는 실제 사용자 IP보다 더 빨리 차단됩니다 — 프록시 유형 선택 섹션에서 자세히 설명합니다.

reqwest에서 HTTP/HTTPS 프록시 기본 설정

reqwest는 Proxy 구조체를 통해 프록시를 지원하며, 이를 ClientBuilder에 전달해야 합니다. Cargo.toml에 종속성을 추가하세요:

[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }

로그인 및 비밀번호 인증이 필요한 HTTP 프록시의 최소 예제:

use reqwest::{Client, Proxy};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let proxy = Proxy::all("http://gate.proxycove.com:8000")?
        .basic_auth("user_login", "user_password");

    let client = Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(15))
        .build()?;

    let resp = client
        .get("https://httpbin.org/ip")
        .send()
        .await?
        .text()
        .await?;

    println!("프록시를 통한 응답: {resp}");
    Ok(())
}

Proxy::all 메소드는 HTTP 및 HTTPS 트래픽을 모두 프록시를 통해 전달합니다. 프로토콜을 분리해야 하는 경우, Proxy::http() 및 Proxy::https() 를 별도로 사용하세요 — 이는 프록시 제공자가 서로 다른 포트를 다른 프로토콜에 대해 수신하는 경우 유용합니다.

SOCKS5 프록시 연결

SOCKS5는 HTTP 프록시보다 더 낮은 수준에서 작동하며, 요청의 내용을 분석하지 않고 TCP 연결을 지원합니다 — 이는 특히 HTTP(S)뿐만 아니라 WebSocket 엔드포인트를 호출하는 경우 파싱에 더 빠르고 안정적입니다. SOCKS5를 사용하려면 Cargo.toml에서 socks 기능을 반드시 활성화해야 합니다 (위에서 이미 언급했습니다). 연결 예제는 다음과 같습니다:

use reqwest::{Client, Proxy};

async fn build_socks5_client(
    host: &str,
    login: &str,
    password: &str,
) -> reqwest::Result<Client> {
    let proxy_url = format!("socks5h://{host}");
    let proxy = Proxy::all(&proxy_url)?.basic_auth(login, password);

    Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(20))
        .build()
}

socks5h 스킴을 사용해야 한다는 점에 유의하세요 socks5 대신. 끝의 "h"는 도메인 DNS 해석이 로컬이 아니라 프록시 서버 측에서 발생함을 의미합니다. 이는 익명성에 매우 중요합니다: 그렇지 않으면 실제 DNS 해석기가 요청된 도메인을 프록시를 우회하여 "유출"할 수 있습니다, 비록 트래픽 자체가 SOCKS5 터널을 통해 이동하더라도 말입니다.

프록시 제공자가 로그인/비밀번호 인증을 요구하지 않는 경우 (IP 화이트리스트), basic_auth를 호출하지 마세요 — 제공자의 패널에 요청이 발생하는 서버의 IP 주소를 지정하세요.

다양한 프록시를 위한 reqwest 클라이언트 풀

자주 발생하는 실수는 각 요청마다 새로운 Client를 생성하는 것입니다. 이는 비용이 많이 듭니다: reqwest는 내부적으로 TCP 연결 및 TLS 세션 풀을 유지하며, 클라이언트를 재생성하면 keep-alive의 모든 이점이 사라집니다. 올바른 접근 방식은 각 프록시마다 미리 하나의 클라이언트를 생성하고 이를 공용 풀에서 재사용하는 것입니다.

use reqwest::{Client, Proxy};
use std::sync::Arc;

#[derive(Clone)]
struct ProxyEndpoint {
    url: String,
    login: String,
    password: String,
}

struct ClientPool {
    clients: Vec<Client>,
}

impl ClientPool {
    fn build(endpoints: &[ProxyEndpoint]) -> reqwest::Result<Self> {
        let mut clients = Vec::with_capacity(endpoints.len());

        for ep in endpoints {
            let proxy = Proxy::all(&ep.url)?
                .basic_auth(&ep.login, &ep.password);

            let client = Client::builder()
                .proxy(proxy)
                .pool_max_idle_per_host(4)
                .timeout(std::time::Duration::from_secs(15))
                .build()?;

            clients.push(client);
        }

        Ok(Self { clients })
    }
}

이러한 풀은 파서 시작 시 한 번 생성됩니다. 클라이언트 수는 일반적으로 풀에 있는 고유한 프록시 주소 수와 같습니다 — 제공자가 게이트웨이 수준에서 회전을 제공하는 경우 (gateway), 하나 또는 두 개의 클라이언트면 충분합니다, 왜냐하면 IP 회전은 매 새로운 TCP 연결 시 제공자 측에서 자동으로 발생하기 때문입니다.

높은 병렬성을 요구하는 작업의 경우 Arc로 풀을 감싸고 tokio 작업 간에 공유하세요 — reqwest 클라이언트는 스레드 안전하며 동시 사용을 위해 설계되었습니다.

IP 회전 전략

회전에는 두 가지 근본적으로 다른 접근 방식이 있습니다. 첫 번째는 제공자 측 회전입니다: 하나의 게이트웨이 주소에 연결하고, 제공자가 각 요청마다 또는 N분마다 새로운 IP를 자동으로 대입합니다. 두 번째는 사용자 측 회전입니다: 정적 프록시 목록을 유지하고 다음 요청에 사용할 프록시를 직접 선택합니다.

주거용 프록시를 사용하고 게이트웨이를 통해 자동 회전을 설정하면, 코드는 최소한으로 단순화됩니다 — 같은 클라이언트를 통해 요청을 보내기만 하면 되고, 제공자가 자체적으로 출발 IP를 변경합니다. 그러나 제어가 필요하다면 (예: 실제 사용자 행동을 모방하기 위해 세션에 IP를 10분 동안 고정해야 하는 경우), 수동으로 회전을 구현하세요:

use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};

struct RotatingPool {
    clients: Vec<Client>,
    counter: AtomicUsize,
}

impl RotatingPool {
    // 라운드 로빈: 클라이언트를 차례로 순회
    fn next_round_robin(&self) -> &Client {
        let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
        &self.clients[idx]
    }

    // 무작위 선택 — 패턴 분석에 유용
    fn next_random(&self) -> &Client {
        self.clients.choose(&mut rand::thread_rng()).unwrap()
    }
}

라운드 로빈은 모든 IP에 고른 부하가 중요할 때 적합합니다. 무작위 선택은 특정 사이트가 하나의 주소에서 요청 간의 간격을 분석할 경우 요청 패턴을 숨기는 데 더 좋습니다. Ozon의 가격 모니터링이나 Avito의 광고 수집과 같은 작업의 경우, 실제로는 조합이 더 효과적입니다: 무작위 프록시 선택과 동일한 클라이언트의 요청 간에 1-3초의 무작위 지연을 추가하는 것입니다.

스티키 세션에 대해 별도로 언급할 필요가 있습니다. 파서가 다단계 시나리오(로그인 → 페이지 전환 → 작업)를 수행하는 경우, 세션 중간에 IP를 변경할 수 없습니다 — 사이트는 활성 쿠키 세션에서 IP 변경을 감지하고 계정을 로그아웃시키거나 차단할 수 있습니다. 이러한 시나리오에서는 특정 "세션"의 파서에 대해 하나의 클라이언트(및 따라서 하나의 IP)를 고정하세요.

오류 처리 및 재시도 로직

프록시는 가끔씩 작동하지 않습니다: 타임아웃, 407 (프록시 인증 필요), TCP 연결 끊김. 파서는 "프록시가 고장났습니다 — 풀에서 제외"와 "사이트가 429를 반환했습니다 — 그냥 기다리고 IP를 변경하세요"를 구분할 수 있어야 합니다. 다음은 재시도 및 죽은 프록시를 제외하는 작업 패턴입니다:

use reqwest::StatusCode;
use std::time::Duration;

async fn fetch_with_retry(
    pool: &RotatingPool,
    url: &str,
    max_attempts: u32,
) -> Result<String, String> {
    let mut last_error = String::new();

    for attempt in 0..max_attempts {
        let client = pool.next_random();

        match client.get(url).send().await {
            Ok(resp) if resp.status() == StatusCode::TOO_MANY_REQUESTS => {
                last_error = "429: 요청 한도 초과, IP를 변경합니다".into();
                tokio::time::sleep(Duration::from_millis(500 * (attempt as u64 + 1))).await;
                continue;
            }
            Ok(resp) if resp.status().is_success() => {
                return resp.text().await.map_err(|e| e.to_string());
            }
            Ok(resp) => {
                last_error = format!("예상치 못한 상태: {}", resp.status());
            }
            Err(e) if e.is_timeout() || e.is_connect() => {
                last_error = format!("프록시 문제: {e}");
                continue;
            }
            Err(e) => {
                last_error = e.to_string();
            }
        }
    }

    Err(format!("모든 시도가 소진되었습니다: {last_error}"))
}

중요한 점: 지수 대기 시간 (500 * attempt) 는 대량의 429 응답이 발생할 때 대상 사이트에 대한 부하를 줄이고 동시에 전체 프록시 풀의 완전 차단 위험을 줄입니다. 프로덕션 파서의 경우, 풀 내 각 프록시에 대한 오류 카운터를 추가하고, 연속적으로 5회 이상의 오류를 발생시키는 주소를 자동으로 제외하세요 — 이는 이미 차단된 IP를 통해 무의미한 시도를 피하는 데 도움이 됩니다.

파싱을 위한 프록시 유형 선택

프록시 유형 선택은 차단되기 전에 얼마나 많은 성공적인 요청을 받을 수 있는지에 직접적인 영향을 미칩니다. 데이터 센터 IP는 가장 저렴하고 빠르지만, 잘 알려진 호스팅 제공업체의 범위에 속합니다 — 마켓플레이스 및 소셜 미디어의 안티봇 시스템은 이를 거의 즉시 ASN 데이터베이스를 통해 감지합니다. 주거용 및 모바일 IP는 일반 사용자처럼 보이므로 더 오랫동안 살아남습니다.

프록시 유형 속도 차단 위험 언제 사용해야 하는가
데이터 센터 높음 높음 열린 API, 안티봇이 없는 사이트
주거용 중간 낮음 Wildberries, Ozon, Avito, 소셜 미디어
모바일 중간 최소 Instagram, TikTok, 엄격한 안티봇

대량의 마켓플레이스 파싱을 위해서는 주거용 프록시가 합리적인 균형을 이룹니다: 모바일보다 트래픽당 비용이 덜 비쌉니다. 그러나 데이터 센터 범위를 차단하는 "실제 사용자" 수준의 검사를 통과합니다. 만약 파서가 소셜 미디어 API를 통해 작동하거나 모바일 애플리케이션을 모방한다면, 모바일 프록시는 통신사에 고정된 IP 덕분에 캡차 및 임시 차단의 비율이 가장 낮습니다.

성능 및 부하에 대한 조언

Rust는 하나의 인스턴스에서 수천 개의 동시 요청을 유지할 수 있지만, 프록시 제공자와 대상 사이트는 여전히 실제 대역폭을 제한합니다. 프로덕션 파서를 위한 몇 가지 실용적인 규칙:

  • tokio::sync::Semaphore를 통해 병렬성을 제한하세요 — 하나의 프록시 풀에 대해 20-50개의 동시 요청을 초과하지 마세요, 그렇지 않으면 일부 연결이 타임아웃으로 끊어질 수 있습니다.
  • pool_idle_timeout을 ClientBuilder에서 사용하여 reqwest가 불안정한 프록시를 통해 죽은 연결을 유지하지 않도록 하세요.
  • 각 프록시별로 HTTP 상태 및 응답 시간을 기록하세요 — 이렇게 하면 어떤 주소가 "썩기" 시작했는지 빠르게 확인하고 교체할 수 있습니다.
  • IP 회전과 함께 User-Agent를 회전하면 탐지 가능성을 크게 줄입니다: 이를 동기화하여 변경하고, 개별 주기로 변경하지 마세요.
  • 장기 생존하는 파서를 위해 헬스 체크를 추가하세요: 몇 분마다 각 프록시를 통해 https://httpbin.org/ip에 테스트 요청을 보내고 응답하지 않는 프록시를 제외하세요.

TLS 핑거프린트(JA3/JA3S)에 특별한 주의를 기울이세요. reqwest는 기본적으로 rustls 또는 nativetls를 사용하며, 이는 빌드 기능에 따라 다릅니다. 두 가지 모두 TLS 핸드셰이크의 핑거프린트가 다릅니다. 일부 안티봇 시스템(예: 대형 마켓플레이스)은 IP뿐만 아니라 TLS 핑거프린트를 분석합니다 — 따라서 완벽한 프록시라도 클라이언트가 비표준 TLS 핸드셰이크를 제공하면 구제되지 않습니다. "깨끗한" 프록시에서 차단되는 경우, reqwest 기능을 확인하고 rustls 대신 native-tls 기능을 시도하거나 그 반대의 경우도 시도해 보세요.

결론

Rust reqwest에서 프록시는 빠르게 설정됩니다 — Proxy::all 또는 SOCKS5 스킴 socks5h://로 몇 줄이면 됩니다. 주요 작업은 그 다음에 시작됩니다: 클라이언트 풀 구축, 회전 전략 선택(라운드 로빈, 무작위 또는 스티키 세션) 및 오류 처리 및 비작동 주소를 풀에서 제외하는 것입니다. 이를 수행하지 않으면 가장 빠른 Rust 파서도 몇 분 내에 차단될 것입니다.

마켓플레이스나 소셜 미디어와 함께 작업하는 경우, 안티봇 보호가 요청 빈도뿐만 아니라 IP 출처를 분석하는 경우, 주거용 프록시로 시작하세요 — 이들은 가격과 세션 생존성의 최상의 균형을 제공합니다. Instagram 및 TikTok과 같은 더 엄격한 목표를 위해서는 모바일 IP를 테스트하고, 안티봇이 없는 간단한 작업에는 빠르고 저렴한 데이터 센터 프록시로 충분합니다.