PHP로 웹 스크래핑이나 타사 API 연동을 하다 보면, 단일 IP로 여러 요청을 보낼 때 차단되는 경험이 있을 것입니다. 레이트 리밋, 지역 제한, 봇 탐지—이 모든 것이 프록시 없이는 극복하기 어렵습니다. 이 가이드는 PHP 개발자가 HTTP 프록시를 올바르게 설정하고, 실무 프로젝트에 통합하는 모든 방법을 코드 위주로 설명합니다.
cURL로 프록시 사용하기: 기본부터 시작
PHP의 curl_* 함수는 가장 낮은 수준의 HTTP 클라이언트입니다. 모든 옵션을 직접 제어할 수 있어, 프록시 설정이 어떻게 동작하는지 이해하기 좋습니다.
핵심 옵션은 두 가지입니다:
CURLOPT_PROXY: 프록시 서버 주소 (호스트:포트)CURLOPT_PROXYUSERPWD: 인증 정보 (username:password)
<?php
function fetchWithProxy(string $url, string $username, string $password): string
{
$ch = curl_init();
// 기본 cURL 옵션
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
// 프록시 설정 (ProxyHat 게이트웨이)
CURLOPT_PROXY => 'gate.proxyhat.com',
CURLOPT_PROXYPORT => 8080,
CURLOPT_PROXYUSERPWD => sprintf('%s:%s', $username, $password),
// 프록시 타입: HTTP
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
// TLS 검증 (프로덕션에서는 반드시 활성화)
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException('cURL error: ' . $error);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
throw new RuntimeException("HTTP error: {$httpCode}");
}
return $response;
}
// 사용 예시
try {
$html = fetchWithProxy(
'https://httpbin.org/ip',
'user-country-US', // ProxyHat 사용자명 (지역 타겟팅 포함)
'your-password'
);
echo $html;
} catch (RuntimeException $e) {
echo "Error: " . $e->getMessage();
}
프록시 인증과 지역 타겟팅
ProxyHat은 사용자명에 플래그를 포함해 지역 타겟팅과 세션 관리를 지원합니다:
<?php
// 국가 지정 (미국 IP)
$username = 'user-country-US';
// 도시 수준 타겟팅 (독일 베를린)
$username = 'user-country-DE-city-berlin';
// 스티키 세션 (동일 IP 유지)
$username = 'user-session-abc123-country-US';
// 프록시 URL 형식으로 직접 사용
$proxyUrl = sprintf(
'http://%s:%s@gate.proxyhat.com:8080',
$username,
$password
);
Guzzle HTTP로 프록시 사용하기
Guzzle은 PHP에서 가장 널리 쓰이는 HTTP 클라이언트 라이브러리입니다. PSR-7을 준수하고, 미들웨어, 비동기 요청, 재시도 로직 등을 지원합니다.
기본 프록시 설정
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class ProxyClient
{
private Client $client;
private string $username;
private string $password;
public function __construct(string $username, string $password)
{
$this->username = $username;
$this->password = $password;
$this->client = new Client([
'base_uri' => 'https://httpbin.org',
'timeout' => 30,
'connect_timeout' => 10,
// 기본 프록시 설정
'proxy' => sprintf(
'http://%s:%s@gate.proxyhat.com:8080',
$username,
$password
),
// TLS 검증
'verify' => true,
// 기본 헤더
'headers' => [
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
],
]);
}
public function get(string $uri, array $options = []): string
{
try {
$response = $this->client->get($uri, $options);
return (string) $response->getBody();
} catch (RequestException $e) {
// 재시도 로직이나 로깅을 여기에 추가
throw $e;
}
}
public function post(string $uri, array $data = [], array $options = []): string
{
$options['form_params'] = $data;
$response = $this->client->post($uri, $options);
return (string) $response->getBody();
}
}
// 사용
$client = new ProxyClient('user-country-US', 'your-password');
$body = $client->get('/ip');
echo $body;
요청별 IP 회전
각 요청마다 다른 프록시 IP를 사용하려면, 요청 옵션에서 proxy를 오버라이드합니다:
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
class RotatingProxyClient
{
private Client $client;
private array $proxyCredentials;
private int $currentIndex = 0;
public function __construct(array $usernames, string $password)
{
// 여러 사용자명으로 로테이션 (각각 다른 IP 풀)
$this->proxyCredentials = array_map(
fn($user) => ['username' => $user, 'password' => $password],
$usernames
);
$this->client = new Client([
'timeout' => 30,
'verify' => true,
]);
}
private function getNextProxyUrl(): string
{
$cred = $this->proxyCredentials[$this->currentIndex];
$this->currentIndex = ($this->currentIndex + 1) % count($this->proxyCredentials);
return sprintf(
'http://%s:%s@gate.proxyhat.com:8080',
$cred['username'],
$cred['password']
);
}
public function fetchWithRotation(string $url): string
{
$response = $this->client->get($url, [
'proxy' => $this->getNextProxyUrl(),
]);
return (string) $response->getBody();
}
// 여러 URL 동시 요청 (각각 다른 프록시)
public function fetchMultiple(array $urls): array
{
$requests = array_map(function($url) {
return new Request('GET', $url);
}, $urls);
$results = [];
$proxyIndex = 0;
$pool = new Pool($this->client, $requests, [
'concurrency' => 5,
'options' => [
'proxy' => $this->proxyCredentials[$proxyIndex++ % count($this->proxyCredentials)],
],
'fulfilled' => function ($response, $index) use (&$results, $urls) {
$results[$urls[$index]] = (string) $response->getBody();
},
'rejected' => function ($reason, $index) use (&$results, $urls) {
$results[$urls[$index]] = ['error' => $reason->getMessage()];
},
]);
$promise = $pool->promise();
$promise->wait();
return $results;
}
}
// 사용
$client = new RotatingProxyClient(
['user-country-US', 'user-country-DE', 'user-country-GB'],
'your-password'
);
// 각 요청이 다른 국가 IP를 사용
$html = $client->fetchWithRotation('https://example.com/data');
Symfony HTTP Client로 비동기 요청
Symfony HTTP Client는 PHP 8.1+에서 네이티브 비동기 지원을 제공합니다. AMPHP나 ReactPHP 없이도 스트리밍과 동시 요청이 가능합니다.
<?php
require 'vendor/autoload.php';
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\ResponseInterface;
class SymfonyProxyClient
{
private $client;
private string $proxyUrl;
public function __construct(string $username, string $password)
{
$this->proxyUrl = sprintf(
'http://%s:%s@gate.proxyhat.com:8080',
$username,
$password
);
$this->client = HttpClient::create([
'timeout' => 30,
'max_redirects' => 5,
'verify_peer' => true,
'verify_host' => true,
]);
}
public function fetch(string $url): string
{
$response = $this->client->request('GET', $url, [
'proxy' => $this->proxyUrl,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (compatible; ProxyBot/1.0)',
],
]);
return $response->getContent();
}
// 비동기 스트리밍 (대용량 응답용)
public function fetchStream(string $url, callable $onChunk): void
{
$response = $this->client->request('GET', $url, [
'proxy' => $this->proxyUrl,
]);
foreach ($this->client->stream($response) as $chunk) {
$onChunk($chunk->getContent());
}
}
// 동시 요청 (Concurrent)
public function fetchConcurrent(array $urls): array
{
$responses = [];
$results = [];
// 모든 요청 시작 (논블로킹)
foreach ($urls as $key => $url) {
$responses[$key] = $this->client->request('GET', $url, [
'proxy' => $this->proxyUrl,
]);
}
// 모든 응답 대기
foreach ($responses as $key => $response) {
try {
$results[$key] = $response->getContent();
} catch (\Exception $e) {
$results[$key] = ['error' => $e->getMessage()];
}
}
return $results;
}
// AsyncResponse로 세밀한 제어
public function fetchWithRetry(string $url, int $maxRetries = 3): string
{
$attempts = 0;
$response = new AsyncResponse(
$this->client,
'GET',
$url,
function ($method, $url, $options) use (&$attempts, $maxRetries) {
$options['proxy'] = $this->proxyUrl;
return function ($chunk) use (&$attempts, $maxRetries, $url, $options) {
// 에러 발생 시 재시도 로직
if (is_resource($chunk) && feof($chunk)) {
if ($attempts < $maxRetries) {
$attempts++;
// 재시도 로직...
}
}
yield $chunk;
};
}
);
return $response->getContent();
}
}
// 사용
$client = new SymfonyProxyClient('user-country-US', 'your-password');
// 단일 요청
$content = $client->fetch('https://httpbin.org/ip');
// 동시 요청
$results = $client->fetchConcurrent([
'ip' => 'https://httpbin.org/ip',
'headers' => 'https://httpbin.org/headers',
'user-agent' => 'https://httpbin.org/user-agent',
]);
print_r($results);
Laravel 통합: 프록시 풀 서비스 클래스
Laravel 프로젝트에서는 서비스 컨테이너와 파사드를 활용해 재사용 가능한 프록시 클라이언트를 만들 수 있습니다. 잡(Jobs)에서도 안전하게 사용할 수 있도록 설계합니다.
<?php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class ResidentialProxyPool
{
private Client $client;
private string $password;
private array $countries;
private bool $useStickySession;
private ?string $sessionId;
// 재시도 설정
private int $maxRetries = 3;
private int $retryDelay = 1000; // ms
// 서킷 브레이커
private int $failureThreshold = 5;
private int $recoveryTimeout = 60; // seconds
public function __construct(
?string $password = null,
array $countries = ['US'],
bool $useStickySession = false,
?string $sessionId = null
) {
$this->password = $password ?? config('proxyhat.password');
$this->countries = $countries;
$this->useStickySession = $useStickySession;
$this->sessionId = $sessionId ?? $this->generateSessionId();
$this->client = new Client([
'timeout' => config('proxyhat.timeout', 30),
'connect_timeout' => config('proxyhat.connect_timeout', 10),
'verify' => config('proxyhat.verify_ssl', true),
]);
}
private function generateSessionId(): string
{
return 'session-' . bin2hex(random_bytes(8));
}
private function buildProxyUrl(string $country = null): string
{
$country = $country ?? $this->countries[array_rand($this->countries)];
$username = 'user-country-' . strtoupper($country);
if ($this->useStickySession) {
$username .= '-' . $this->sessionId;
}
return sprintf(
'http://%s:%s@gate.proxyhat.com:8080',
$username,
$this->password
);
}
private function isCircuitOpen(): bool
{
$failures = Cache::get('proxy_failures', 0);
$lastFailure = Cache::get('proxy_last_failure', 0);
if ($failures >= $this->failureThreshold) {
if (time() - $lastFailure < $this->recoveryTimeout) {
return true; // 서킷 오픈
}
// 복구 시간 경과, 리셋
Cache::forget('proxy_failures');
}
return false;
}
private function recordFailure(): void
{
$failures = Cache::increment('proxy_failures');
Cache::put('proxy_last_failure', time(), 300);
Log::warning("Proxy failure recorded", ['count' => $failures]);
}
private function recordSuccess(): void
{
Cache::forget('proxy_failures');
}
public function request(
string $method,
string $url,
array $options = [],
?string $country = null
): string {
// 서킷 브레이커 체크
if ($this->isCircuitOpen()) {
throw new \RuntimeException('Proxy circuit breaker is open');
}
$proxyUrl = $this->buildProxyUrl($country);
$options['proxy'] = $proxyUrl;
$lastError = null;
for ($attempt = 0; $attempt < $this->maxRetries; $attempt++) {
try {
$response = $this->client->request($method, $url, $options);
$this->recordSuccess();
return (string) $response->getBody();
} catch (RequestException $e) {
$lastError = $e->getMessage();
Log::debug("Proxy request failed", [
'attempt' => $attempt + 1,
'error' => $e->getMessage(),
'url' => $url,
]);
// 429나 403은 재시도 의미 없음
if (in_array($e->getCode(), [429, 403])) {
break;
}
usleep($this->retryDelay * 1000 * ($attempt + 1));
}
}
$this->recordFailure();
throw new \RuntimeException("Proxy request failed after {$this->maxRetries} attempts: " . $lastError);
}
public function get(string $url, array $options = [], ?string $country = null): string
{
return $this->request('GET', $url, $options, $country);
}
public function post(string $url, array $data = [], array $options = [], ?string $country = null): string
{
$options['json'] = $data;
return $this->request('POST', $url, $options, $country);
}
// 국가별 요청 (분산 수집용)
public function fetchFromMultipleCountries(string $url, array $countries): array
{
$results = [];
foreach ($countries as $country) {
try {
$results[$country] = $this->get($url, [], $country);
} catch (\Exception $e) {
$results[$country] = ['error' => $e->getMessage()];
}
}
return $results;
}
}
Laravel 설정 파일
<?php
// config/proxyhat.php
return [
'password' => env('PROXYHAT_PASSWORD'),
'timeout' => env('PROXYHAT_TIMEOUT', 30),
'connect_timeout' => env('PROXYHAT_CONNECT_TIMEOUT', 10),
'verify_ssl' => env('PROXYHAT_VERIFY_SSL', true),
'default_countries' => ['US', 'DE', 'GB'],
'sticky_sessions' => env('PROXYHAT_STICKY_SESSIONS', false),
];
Laravel Job에서 사용하기
<?php
namespace App\Jobs;
use App\Services\ResidentialProxyPool;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ScrapeProductData implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 60;
private string $url;
private string $country;
public function __construct(string $url, string $country = 'US')
{
$this->url = $url;
$this->country = $country;
}
public function handle(ResidentialProxyPool $proxyPool): void
{
// 스티키 세션으로 Job 전체에서 동일 IP 사용
$proxyPool->__construct(
password: config('proxyhat.password'),
countries: [$this->country],
useStickySession: true,
sessionId: 'job-' . $this->job->getJobId()
);
try {
$html = $proxyPool->get($this->url);
// HTML 파싱 및 데이터 추출
$data = $this->parseHtml($html);
// 결과 저장
$this->saveData($data);
Log::info("Scraping completed", [
'url' => $this->url,
'country' => $this->country,
]);
} catch (\Exception $e) {
Log::error("Scraping failed", [
'url' => $this->url,
'error' => $e->getMessage(),
]);
throw $e; // 재시도를 위해 예외 다시 던짐
}
}
private function parseHtml(string $html): array
{
// 파싱 로직...
return [];
}
private function saveData(array $data): void
{
// 저장 로직...
}
}
// Job 디스패치
ScrapeProductData::dispatch('https://example.com/product/123', 'US')
->onQueue('scraping');
서비스 프로바이더 등록
<?php
namespace App\Providers;
use App\Services\ResidentialProxyPool;
use Illuminate\Support\ServiceProvider;
class ProxyServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(ResidentialProxyPool::class, function ($app) {
return new ResidentialProxyPool(
password: config('proxyhat.password'),
countries: config('proxyhat.default_countries'),
useStickySession: config('proxyhat.sticky_sessions'),
);
});
}
}
// config/app.php의 providers에 추가
// App\Providers\ProxyServiceProvider::class,
multi_curl로 동시 요청 처리
PHP의 curl_multi_* 함수를 사용하면 여러 HTTP 요청을 병렬로 실행할 수 있습니다. 대량 스크래핑에 필수적입니다.
<?php
class ConcurrentProxyFetcher
{
private string $proxyHost;
private int $proxyPort;
private string $username;
private string $password;
public function __construct(string $username, string $password)
{
$this->proxyHost = 'gate.proxyhat.com';
$this->proxyPort = 8080;
$this->username = $username;
$this->password = $password;
}
/**
* 여러 URL을 동시에 가져오기
*
* @param array $urls URL 배열
* @param int $concurrency 동시 연결 수
* @return array [url => response] 형태의 결과
*/
public function fetchAll(array $urls, int $concurrency = 10): array
{
$results = [];
$handles = [];
// cURL 멀티 핸들 생성
$mh = curl_multi_init();
// 각 URL에 대한 cURL 핸들 생성 및 추가
foreach ($urls as $key => $url) {
$ch = $this->createHandle($url);
$handles[$key] = $ch;
curl_multi_add_handle($mh, $ch);
}
// 실행
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh); // I/O 대기
} while ($running > 0);
// 결과 수집
foreach ($handles as $key => $ch) {
$error = curl_error($ch);
if ($error) {
$results[$key] = ['error' => $error];
} else {
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$content = curl_multi_getcontent($ch);
$results[$key] = [
'status' => $httpCode,
'body' => $content,
];
}
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi_close($mh);
return $results;
}
/**
* 대량 URL 처리 (청크 단위)
*/
public function fetchBatch(array $urls, int $batchSize = 20): array
{
$allResults = [];
$chunks = array_chunk($urls, $batchSize, true);
foreach ($chunks as $chunk) {
$results = $this->fetchAll($chunk, $batchSize);
$allResults = array_merge($allResults, $results);
// 서버 부하 방지
usleep(500000); // 0.5초 대기
}
return $allResults;
}
private function createHandle(string $url): \CurlHandle
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 5,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
// 프록시 설정
CURLOPT_PROXY => $this->proxyHost,
CURLOPT_PROXYPORT => $this->proxyPort,
CURLOPT_PROXYUSERPWD => sprintf('%s:%s', $this->username, $this->password),
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
// TLS
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
// 성능 최적화
CURLOPT_ENCODING => 'gzip, deflate',
CURLOPT_TCP_FASTOPEN => true,
]);
return $ch;
}
}
// 사용 예시
$fetcher = new ConcurrentProxyFetcher('user-country-US', 'your-password');
$urls = [
'page1' => 'https://httpbin.org/ip',
'page2' => 'https://httpbin.org/headers',
'page3' => 'https://httpbin.org/user-agent',
];
$results = $fetcher->fetchAll($urls);
foreach ($results as $key => $result) {
if (isset($result['error'])) {
echo "{$key}: Error - {$result['error']}\n";
} else {
echo "{$key}: HTTP {$result['status']}\n";
echo substr($result['body'], 0, 100) . "...\n\n";
}
}
TLS/SSL 설정과 CA 번들 관리
프록시를 통한 HTTPS 요청에서 인증서 검증은 보안의 핵심입니다. 잘못된 CA 설정은 MITM 공격에 노출될 수 있습니다.
CA 번들 설정
<?php
class SecureProxyClient
{
private string $caBundlePath;
private string $proxyUrl;
public function __construct(string $username, string $password)
{
$this->proxyUrl = sprintf(
'http://%s:%s@gate.proxyhat.com:8080',
$username,
$password
);
// CA 번들 경로 설정
$this->caBundlePath = $this->getCaBundlePath();
}
private function getCaBundlePath(): string
{
// 1. Composer로 설치된 CA 번들 (권장)
$composerCa = dirname(__DIR__, 3) . '/composer/ca-bundle/res/cacert.pem';
if (file_exists($composerCa)) {
return $composerCa;
}
// 2. 시스템 CA 번들
$systemPaths = [
'/etc/ssl/certs/ca-certificates.crt', // Debian/Ubuntu
'/etc/pki/tls/certs/ca-bundle.crt', // RHEL/CentOS
'/etc/ssl/ca-bundle.pem', // OpenSUSE
'/usr/local/etc/openssl/cert.pem', // macOS Homebrew
];
foreach ($systemPaths as $path) {
if (file_exists($path)) {
return $path;
}
}
// 3. Mozilla CA 번들 다운로드
return $this->downloadCaBundle();
}
private function downloadCaBundle(): string
{
$cachePath = sys_get_temp_dir() . '/cacert.pem';
// 캐시가 있고 30일 이내면 재사용
if (file_exists($cachePath) &&
(time() - filemtime($cachePath)) < 30 * 86400) {
return $cachePath;
}
$caContent = file_get_contents(
'https://curl.se/ca/cacert.pem',
false,
stream_context_create([
'ssl' => ['verify_peer' => false], // 초기 다운로드만
])
);
if ($caContent === false) {
throw new RuntimeException('Failed to download CA bundle');
}
file_put_contents($cachePath, $caContent);
return $cachePath;
}
public function createGuzzleClient(): \GuzzleHttp\Client
{
return new \GuzzleHttp\Client([
'proxy' => $this->proxyUrl,
'timeout' => 30,
'verify' => $this->caBundlePath, // CA 번들 지정
// 추가 TLS 옵션
'curl' => [
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_SSL_CIPHER_LIST => 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
],
]);
}
// cURL 직접 사용 시
public function createCurlHandle(string $url): \CurlHandle
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'gate.proxyhat.com',
CURLOPT_PROXYPORT => 8080,
CURLOPT_PROXYUSERPWD => 'user-country-US:password',
// TLS/SSL 설정
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => $this->caBundlePath,
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
// 인증서 검증 실패 시 상세 에러
CURLOPT_CERTINFO => true,
CURLOPT_VERBOSE => true,
]);
return $ch;
}
// 호스트 검증 커스터마이징
public function fetchWithHostVerification(string $url): string
{
$client = new \GuzzleHttp\Client([
'proxy' => $this->proxyUrl,
'verify' => true,
]);
try {
$response = $client->get($url, [
'headers' => [
'User-Agent' => 'Mozilla/5.0 (compatible; SecureBot/1.0)',
],
]);
return (string) $response->getBody();
} catch (\GuzzleHttp\Exception\RequestException $e) {
// SSL 에러 상세 처리
$context = $e->getHandlerContext();
if (isset($context['ssl_verify_result'])) {
throw new RuntimeException(
'SSL verification failed: ' . ($context['error'] ?? 'Unknown SSL error')
);
}
throw $e;
}
}
}
// 사용
$client = new SecureProxyClient('user-country-US', 'your-password');
$guzzle = $client->createGuzzleClient();
$response = $guzzle->get('https://example.com');
echo $response->getBody();
자체 서명 인증서 처리 (내부 네트워크)
<?php
// 개발/테스트 환경에서만 사용 (프로덕션 금지)
class DevProxyClient
{
public function fetchInsecure(string $url): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'gate.proxyhat.com',
CURLOPT_PROXYPORT => 8080,
CURLOPT_PROXYUSERPWD => 'user-country-US:password',
// 개발 환경에서만 검증 비활성화
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) {
throw new RuntimeException(curl_error($ch));
}
curl_close($ch);
return $result;
}
}
PHP HTTP 클라이언트 비교
| 기능 | cURL (원시) | Guzzle | Symfony HTTP |
|---|---|---|---|
| 설치 | 내장 | Composer 필요 | Composer 필요 |
| 프록시 설정 | 수동 옵션 | 배열 설정 | 배열 설정 |
| 비동기 지원 | multi_curl | Promise (Guzzle 6) | 네이티브 스트림 |
| 미들웨어 | 없음 | 풍부한 생태계 | Symfony 통합 |
| PSR-7 준수 | 아니오 | 예 | 예 |
| 재시도 로직 | 수동 구현 | 미들웨어 가능 | 콜백 가능 |
| Laravel 통합 | 직접 래핑 | 자연스러움 | 자연스러움 |
| 학습 곡선 | 낮음 | 중간 | 중간 |
주요 모범 사례
핵심 요약:
- 프로덕션에서는 반드시
CURLOPT_SSL_VERIFYPEER = true로 설정하세요.- 서킷 브레이커 패턴을 구현해 연속 실패 시 프록시 사용을 일시 중단하세요.
- 스티키 세션은 로그인이나 체크아웃 플로우에, 로테이션은 대량 수집에 사용하세요.
- 국가별 IP로 지역 제한 콘텐츠에 접근할 수 있습니다.
multi_curl이나 Guzzle Pool로 동시 요청을 처리하세요.- CA 번들을 최신으로 유지하고, 만료 인증서를 모니터링하세요.
프록시 사용 시 체크리스트
- 타임아웃 설정: 프록시를 거치면 응답이 느려질 수 있습니다. 연결 타임아웃과 전체 타임아웃을 별도로 설정하세요.
- 에러 로깅: 프록시 연결 실패, 타임아웃, HTTP 에러 코드를 구분해 로깅하세요.
- 재시도 전략: 429(Too Many Requests)는 재시도하지 말고, 502/503은 지수 백오프로 재시도하세요.
- 세션 관리: 로그인이 필요한 사이트는 스티키 세션과 쿠키 저장을 함께 사용하세요.
- 법적 고려:
robots.txt를 존중하고, 이용약관을 확인하며, 개인정보 보호 규정을 준수하세요.
결론
PHP로 HTTP 프록시를 사용하는 것은 복잡하지 않지만, 프로덕션에서 안정적으로 운영하려면 신경 써야 할 부분이 많습니다. 원시 cURL은 가장 낮은 수준의 제어를 제공하고, Guzzle은 풍부한 기능과 미들웨어 생태계를, Symfony HTTP Client는 네이티브 비동기 지원을 제공합니다.
Laravel 프로젝트에서는 서비스 클래스로 프록시 풀을 추상화하고, 서킷 브레이커와 재시도 로직을 구현해, 잡(Jobs)에서 안전하게 사용할 수 있습니다. TLS 검증을 항상 활성화하고, CA 번들을 최신으로 유지하는 것은 보안의 기본입니다.
ProxyHat 가격 페이지에서 레지덴셜, 모바일, 데이터센터 프록시 플랜을 확인하고, 다른 블로그 글에서 더 많은 활용 사례를 찾아보세요.






