Ruby 개발자라면 웹 스크래핑, API 호출, 데이터 파이프라인 구축 중 IP 차단 문제에 직면했을 것입니다. 프록시는 이 문제의 핵심 해결책입니다. 이 가이드에서는 Ruby의 표준 라이브러리부터 고성능 병렬 처리, 그리고 ProxyHat SDK까지 실전 코드와 함께 살펴봅니다.
왜 Ruby에서 프록시가 필요한가
대규모 데이터 수집 시 단일 IP로 요청을 보내면 다음과 같은 문제가 발생합니다:
- Rate limiting: 분당 요청 수 제한으로 429 에러
- IP 차단: 의심스러운 활동으로 인한 영구 차단
- 지역 제한: 특정 국가에서만 접근 가능한 콘텐츠
- 봇 탐지: CAPTCHA 또는 JavaScript 챌린지
프록시를 사용하면 IP를 순환시키고, 지역을 변경하며, 요청을 분산시켜 이러한 제약을 우회할 수 있습니다.
Net::HTTP로 기본 프록시 사용하기
Ruby 표준 라이브러리인 Net::HTTP는 추가 gem 없이 프록시를 지원합니다. 가장 기본적인 형태부터 시작하겠습니다.
기본 프록시 설정
require 'net/http'
require 'uri'
# ProxyHat 연결 정보
PROXY_HOST = 'gate.proxyhat.com'
PROXY_PORT = 8080
PROXY_USER = 'user-country-US'
PROXY_PASS = 'your_password'
def fetch_with_proxy(url, proxy_user: PROXY_USER, proxy_pass: PROXY_PASS)
uri = URI.parse(url)
proxy_uri = URI.parse("http://#{PROXY_HOST}:#{PROXY_PORT}")
http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port)
http.proxy_user = proxy_user
http.proxy_pass = proxy_pass
# TLS/SSL 설정
if uri.scheme == 'https'
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.ca_file = '/etc/ssl/certs/ca-certificates.crt' # 시스템 CA 번들
end
# 타임아웃 설정
http.open_timeout = 10
http.read_timeout = 30
request = Net::HTTP::Get.new(uri.request_uri)
request['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
request['Accept'] = 'text/html,application/xhtml+xml'
response = http.request(request)
case response
when Net::HTTPSuccess
{ success: true, status: response.code, body: response.body, headers: response.each_header.to_h }
when Net::HTTPRedirection
{ success: true, status: response.code, location: response['location'], body: response.body }
when Net::HTTPTooManyRequests
{ success: false, status: response.code, retry_after: response['retry-after'], error: 'Rate limited' }
else
{ success: false, status: response.code, error: response.message }
end
rescue Net::OpenTimeout => e
{ success: false, error: "Connection timeout: #{e.message}" }
rescue Net::ReadTimeout => e
{ success: false, error: "Read timeout: #{e.message}" }
rescue SocketError => e
{ success: false, error: "DNS resolution failed: #{e.message}" }
rescue OpenSSL::SSL::SSLError => e
{ success: false, error: "SSL error: #{e.message}" }
rescue StandardError => e
{ success: false, error: "Unexpected error: #{e.class} - #{e.message}" }
ensure
http&.finish if http&.started?
end
# 사용 예시
result = fetch_with_proxy('https://httpbin.org/ip')
puts result[:body] if result[:success]
지역 타겟팅과 세션 관리
ProxyHat은 사용자 이름 필드를 통해 지역 타겟팅과 세션 관리를 지원합니다:
require 'net/http'
require 'uri'
class ProxyHatClient
BASE_USER = 'user'
BASE_PASS = 'your_password'
PROXY_HOST = 'gate.proxyhat.com'
PROXY_PORT = 8080
# 국가별 프록시 사용
def self.with_country(country_code)
proxy_user = "#{BASE_USER}-country-#{country_code}"
yield(proxy_user, BASE_PASS)
end
# 도시별 프록시 사용
def self.with_city(country_code, city)
proxy_user = "#{BASE_USER}-country-#{country_code}-city-#{city.downcase.gsub(/\s+/, '-')}"
yield(proxy_user, BASE_PASS)
end
# 스티키 세션 (동일 IP 유지)
def self.with_session(country_code, session_id)
proxy_user = "#{BASE_USER}-country-#{country_code}-session-#{session_id}"
yield(proxy_user, BASE_PASS)
end
def self.build_http(uri, proxy_user, proxy_pass)
proxy_uri = URI.parse("http://#{PROXY_HOST}:#{PROXY_PORT}")
http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port)
http.proxy_user = proxy_user
http.proxy_pass = proxy_pass
http.use_ssl = (uri.scheme == 'https')
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl
http.open_timeout = 15
http.read_timeout = 60
http
end
end
# 사용 예시
ProxyHatClient.with_country('JP') do |user, pass|
puts "일본 IP로 요청: #{user}"
result = fetch_with_proxy('https://httpbin.org/ip', proxy_user: user, proxy_pass: pass)
puts result[:body]
end
ProxyHatClient.with_city('DE', 'Berlin') do |user, pass|
puts "베를린 IP로 요청: #{user}"
end
# 로그인 세션 유지용 스티키 세션
session_id = SecureRandom.hex(8)
ProxyHatClient.with_session('US', session_id) do |user, pass|
puts "세션 #{session_id} - 동일 IP 유지"
end
Typhoeus로 병렬 요청 처리하기
Net::HTTP는 단일 요청에 적합하지만, 수천 개의 URL을 처리하려면 병렬 처리가 필요합니다. Typhoeus는 libcurl 기반으로 Hydra를 통한 동시 요청을 지원합니다.
Typhoeus 기본 설정
require 'typhoeus'
# Typhoeus 글로벌 설정
Typhoeus.configure do |config|
config.user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
config.verbose = false
config.follow_location = true
config.max_redirects = 5
end
# 단일 요청 with 프록시
response = Typhoeus.get(
'https://httpbin.org/ip',
proxy: 'http://user-country-KR:your_password@gate.proxyhat.com:8080',
proxyauth: :basic,
timeout: 30,
connecttimeout: 10,
ssl_verifypeer: true,
ssl_verifyhost: 2
)
puts "Status: #{response.code}"
puts "Body: #{response.body}"
puts "Time: #{response.total_time}s"
Hydra로 대량 병렬 요청
require 'typhoeus'
require 'concurrent'
class ParallelScraper
PROXY_BASE = 'http://user-country-%s:your_password@gate.proxyhat.com:8080'
CONCURRENCY = 50 # 동시 요청 수
RETRY_COUNT = 3
RETRY_DELAY = 2
attr_reader :results, :errors, :stats
def initialize
@results = Concurrent::Array.new
@errors = Concurrent::Array.new
@stats = Concurrent::Hash.new(0)
@mutex = Mutex.new
end
def scrape_urls(urls, country: 'US')
hydra = Typhoeus::Hydra.new(
max_concurrency: CONCURRENCY,
timeout: 60
)
urls.each_with_index do |url, index|
request = build_request(url, country, index)
hydra.queue(request)
end
start_time = Time.now
hydra.run
elapsed = Time.now - start_time
print_stats(elapsed, urls.size)
{ results: @results, errors: @errors, stats: @stats }
end
private
def build_request(url, country, index)
proxy_url = format(PROXY_BASE, country)
request = Typhoeus::Request.new(
url,
method: :get,
proxy: proxy_url,
proxyauth: :basic,
timeout: 30,
connecttimeout: 10,
followlocation: true,
maxredirs: 3,
headers: {
'User-Agent' => random_user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.9',
'Accept-Encoding' => 'gzip, deflate'
},
accept_encoding: 'gzip'
)
request.on_complete do |response|
handle_response(response, url, country, index)
end
request
end
def handle_response(response, url, country, index)
@mutex.synchronize do
@stats[:total] += 1
end
case response.code
when 200..299
@mutex.synchronize do
@results << {
url: url,
status: response.code,
body: response.body,
headers: response.headers,
time: response.total_time,
index: index
}
@stats[:success] += 1
end
when 429
@mutex.synchronize do
@stats[:rate_limited] += 1
@errors << { url: url, status: 429, error: 'Rate limited', retry_after: response.headers['Retry-After'] }
end
when 403
@mutex.synchronize do
@stats[:forbidden] += 1
@errors << { url: url, status: 403, error: 'Access forbidden' }
end
else
@mutex.synchronize do
@stats[:http_error] += 1
@errors << { url: url, status: response.code, error: response.status_message }
end
end
rescue => e
@mutex.synchronize do
@stats[:exception] += 1
@errors << { url: url, error: e.message }
end
end
def random_user_agent
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0'
]
user_agents.sample
end
def print_stats(elapsed, total)
puts "\n=== 스크래핑 결과 ==="
puts "총 요청: #{total}"
puts "성공: #{@stats[:success]}"
puts "실패: #{@errors.size}"
puts "처리 시간: #{elapsed.round(2)}s"
puts "평균 속도: #{(total / elapsed).round(2)} req/s"
puts "상세: #{@stats.inspect}"
end
end
# 실행 예시
urls = 100.times.map { |i| "https://httpbin.org/delay/#{rand(1..3)}?id=#{i}" }
scraper = ParallelScraper.new
result = scraper.scrape_urls(urls, country: 'US')
ProxyHat Ruby SDK로 IP 로테이션 구현하기
ProxyHat SDK를 사용하면 IP 로테이션, 지역 타겟팅, 세션 관리를 더욱 간편하게 구현할 수 있습니다.
require 'net/http'
require 'uri'
require 'json'
require 'securerandom'
class ProxyHatSDK
# ProxyHat 게이트웨이 설정
GATEWAY_HOST = 'gate.proxyhat.com'
HTTP_PORT = 8080
SOCKS5_PORT = 1080
attr_reader :username, :password, :options
# @param username [String] ProxyHat 사용자명
# @param password [String] ProxyHat 비밀번호
# @param options [Hash] 추가 옵션
# @option options [String] :country 국가 코드 (ISO 3166-1 alpha-2)
# @option options [String] :city 도시명
# @option options [String] :session 세션 ID (스티키 세션용)
# @option options [Symbol] :protocol :http 또는 :socks5
def initialize(username, password, options = {})
@base_username = username
@password = password
@options = options
@session_id = options[:session] || generate_session_id
end
# 요청마다 새 IP 사용 (rotating session)
def rotating_username
build_username(session: SecureRandom.hex(8))
end
# 동일 IP 유지 (sticky session, 기본 10분)
def sticky_username
build_username(session: @session_id)
end
# 국가 변경
def with_country(country_code)
self.class.new(@base_username, @password, @options.merge(country: country_code))
end
# 도시 변경
def with_city(city_name)
self.class.new(@base_username, @password, @options.merge(city: city_name))
end
# HTTP 프록시 URL 생성
def http_proxy_url
"http://#{rotating_username}:#{@password}@#{GATEWAY_HOST}:#{HTTP_PORT}"
end
# SOCKS5 프록시 URL 생성
def socks5_proxy_url
"socks5://#{rotating_username}:#{@password}@#{GATEWAY_HOST}:#{SOCKS5_PORT}"
end
# Net::HTTP 호환 설정 반환
def net_http_config
{
proxy_host: GATEWAY_HOST,
proxy_port: (@options[:protocol] == :socks5) ? SOCKS5_PORT : HTTP_PORT,
proxy_user: rotating_username,
proxy_pass: @password
}
end
# Typhoeus 호환 프록시 URL
def typhoeus_proxy
protocol = (@options[:protocol] == :socks5) ? 'socks5' : 'http'
"#{protocol}://#{rotating_username}:#{@password}@#{GATEWAY_HOST}:#{(protocol == 'socks5') ? SOCKS5_PORT : HTTP_PORT}"
end
# 배치 요청용 IP 풀 생성 (각 요청이 다른 IP 사용)
def generate_ip_pool(size)
size.times.map do |i|
build_username(session: "batch-#{i}-#{SecureRandom.hex(4)}")
end
end
private
def build_username(overrides = {})
parts = [@base_username]
country = overrides[:country] || @options[:country]
parts << "country-#{country}" if country
city = overrides[:city] || @options[:city]
parts << "city-#{city.downcase.gsub(/\s+/, '-')}" if city
session = overrides[:session]
parts << "session-#{session}" if session
parts.join('-')
end
def generate_session_id
"sdk-#{SecureRandom.hex(8)}"
end
end
# 사용 예시
proxy = ProxyHatSDK.new('user', 'your_password', country: 'US')
# 요청마다 다른 IP
puts "Rotating proxy: #{proxy.http_proxy_url}"
# 국가 변경
jp_proxy = proxy.with_country('JP')
puts "Japan proxy: #{jp_proxy.http_proxy_url}"
# 도시 타겟팅
berlin_proxy = proxy.with_country('DE').with_city('Berlin')
puts "Berlin proxy: #{berlin_proxy.http_proxy_url}"
# IP 풀 생성 (100개 요청용)
ip_pool = proxy.generate_ip_pool(100)
puts "Generated #{ip_pool.size} unique proxy identities"
실전: 1000개 URL 동시 스크래핑
이제 ProxyHat SDK와 Typhoeus를 결합하여 1000개 URL을 회전형 레지덴셜 프록시로 스크래핑하는 완전한 예제를 살펴보겠습니다.
require 'typhoeus'
require 'concurrent'
require 'json'
require 'benchmark'
class ProductionScraper
include Concurrent::Async
BATCH_SIZE = 100
MAX_CONCURRENCY = 50
RETRY_COUNT = 3
RETRY_DELAY = 2
def initialize(proxy_username, proxy_password)
@proxy_username = proxy_username
@proxy_password = proxy_password
@results = Concurrent::Array.new
@errors = Concurrent::Hash.new
@stats = Concurrent::Hash.new(0)
@circuit_breaker = CircuitBreaker.new(threshold: 10, timeout: 60)
end
# 대량 URL 스크래핑 (국가 분산)
def scrape_with_geo_distribution(urls, countries: ['US', 'DE', 'JP', 'GB', 'FR'])
puts "Starting scrape of #{urls.size} URLs across #{countries.size} countries"
# URL을 국가별로 분산
url_batches = urls.each_slice((urls.size.to_f / countries.size).ceil).to_a
country_batches = countries.cycle.take(url_batches.size)
threads = url_batches.zip(country_batches).map do |batch, country|
Thread.new { process_batch(batch, country) }
end
threads.each(&:join)
generate_report
end
# 단일 배치 처리
def process_batch(urls, country)
hydra = Typhoeus::Hydra.new(max_concurrency: MAX_CONCURRENCY)
urls.each_with_index do |url, idx|
next if @circuit_breaker.open?
session_id = "#{country.downcase}-#{SecureRandom.hex(6)}"
proxy_url = build_proxy_url(country, session_id)
request = Typhoeus::Request.new(
url,
method: :get,
proxy: proxy_url,
proxyauth: :basic,
timeout: 30,
connecttimeout: 15,
followlocation: true,
maxredirs: 3,
headers: build_headers(country)
)
request.on_complete { |response| handle_response(response, url, country) }
hydra.queue(request)
end
hydra.run
rescue => e
puts "Batch error for #{country}: #{e.message}"
@circuit_breaker.record_failure
end
private
def build_proxy_url(country, session_id)
username = "#{@proxy_username}-country-#{country}-session-#{session_id}"
"http://#{username}:#{@proxy_password}@gate.proxyhat.com:8080"
end
def build_headers(country)
accept_lang = case country
when 'DE' then 'de-DE,de;q=0.9,en;q=0.8'
when 'JP' then 'ja-JP,ja;q=0.9,en;q=0.8'
when 'FR' then 'fr-FR,fr;q=0.9,en;q=0.8'
else 'en-US,en;q=0.9'
end
{
'User-Agent' => random_user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => accept_lang,
'Accept-Encoding' => 'gzip, deflate, br',
'DNT' => '1',
'Connection' => 'keep-alive'
}
end
def handle_response(response, url, country)
@stats[:total] += 1
case response.code
when 200..299
@results << {
url: url,
country: country,
status: response.code,
body: response.body,
size: response.body.bytesize,
time: response.total_time
}
@stats[:success] += 1
@circuit_breaker.record_success
when 429
@stats[:rate_limited] += 1
@errors[url] = { status: 429, country: country, retry_after: response.headers['Retry-After'] }
# 재시도 로직
schedule_retry(url, country) if @errors[url][:retry_count].to_i < RETRY_COUNT
when 403
@stats[:forbidden] += 1
@errors[url] = { status: 403, country: country }
when 0
@stats[:network_error] += 1
@errors[url] = { status: 0, country: country, error: response.status_message }
else
@stats[:http_error] += 1
@errors[url] = { status: response.code, country: country }
end
end
def schedule_retry(url, country)
@errors[url][:retry_count] ||= 0
@errors[url][:retry_count] += 1
sleep(RETRY_DELAY * @errors[url][:retry_count])
# 재시도 로직 (실제로는 큐에 다시 넣어야 함)
end
def random_user_agent
@user_agents ||= [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
]
@user_agents.sample
end
def generate_report
total_time = @results.sum { |r| r[:time] }
avg_time = @results.any? ? (total_time / @results.size).round(3) : 0
total_size = @results.sum { |r| r[:size] }
puts "\n" + "=" * 60
puts "스크래핑 완료 리포트"
puts "=" * 60
puts "총 요청: #{@stats[:total]}"
puts "성공: #{@stats[:success]} (#{percent(@stats[:success], @stats[:total])}%)"
puts "실패: #{@errors.size}"
puts "\n실패 상세:"
puts " Rate Limited: #{@stats[:rate_limited]}"
puts " Forbidden (403): #{@stats[:forbidden]}"
puts " Network Errors: #{@stats[:network_error]}"
puts " HTTP Errors: #{@stats[:http_error]}"
puts "\n성능:"
puts " 총 데이터: #{humanize_bytes(total_size)}"
puts " 평균 응답시간: #{avg_time}s"
puts "=" * 60
{ results: @results, errors: @errors, stats: @stats.to_h }
end
def percent(part, total)
return 0 if total.zero?
((part.to_f / total) * 100).round(1)
end
def humanize_bytes(bytes)
units = ['B', 'KB', 'MB', 'GB']
size = bytes.to_f
unit = units.shift
while size > 1024 && units.any?
size /= 1024
unit = units.shift
end
"#{size.round(2)} #{unit}"
end
end
# 서킷 브레이커 구현
class CircuitBreaker
def initialize(threshold:, timeout:)
@threshold = threshold
@timeout = timeout
@failures = 0
@last_failure_time = nil
@mutex = Mutex.new
end
def open?
@mutex.synchronize do
return false if @failures < @threshold
return true unless @last_failure_time
Time.now - @last_failure_time < @timeout
end
end
def record_failure
@mutex.synchronize do
@failures += 1
@last_failure_time = Time.now
end
end
def record_success
@mutex.synchronize { @failures = 0 }
end
end
# 실행
if __FILE__ == $0
# 테스트용 URL 생성 (실제로는 타겟 URL 리스트 사용)
urls = 1000.times.map do |i|
"https://httpbin.org/delay/#{rand(1..2)}?id=#{i}"
end
scraper = ProductionScraper.new('user', 'your_password')
result = scraper.scrape_with_geo_distribution(
urls,
countries: ['US', 'DE', 'JP', 'GB', 'FR', 'CA', 'AU']
)
# 결과를 JSON으로 저장
File.write('scrape_results.json', result[:results].to_json)
end
TLS/SSL 설정과 보안 고려사항
프록시를 통한 HTTPS 요청 시 TLS/SSL 설정이 중요합니다. 특히 자체 서명 인증서, SNI(Server Name Indication), 인증서 검증을 올바르게 처리해야 합니다.
require 'net/http'
require 'uri'
require 'openssl'
class SecureProxyClient
PROXY_HOST = 'gate.proxyhat.com'
PROXY_PORT = 8080
def initialize(username, password)
@proxy_user = username
@proxy_pass = password
end
# 안전한 HTTPS 요청 (기본)
def secure_get(url)
uri = URI.parse(url)
proxy_uri = URI.parse("http://#{PROXY_HOST}:#{PROXY_PORT}")
http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port)
http.proxy_user = @proxy_user
http.proxy_pass = @proxy_pass
if uri.scheme == 'https'
http.use_ssl = true
# 기본: 시스템 CA로 엄격한 검증
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
# SNI 활성화 (가상 호스팅 지원)
http.enable_post_connection_check = true
# OpenSSL 옵션 설정
http.ssl_version = :TLSv1_2
http.ciphers = 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'
end
request = Net::HTTP::Get.new(uri.request_uri)
request['Host'] = uri.host # SNI를 위해 Host 헤더 명시
http.request(request)
end
# 자체 서명 인증서 허용 (개발/테스트 환경)
def insecure_get(url)
uri = URI.parse(url)
proxy_uri = URI.parse("http://#{PROXY_HOST}:#{PROXY_PORT}")
http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port)
http.proxy_user = @proxy_user
http.proxy_pass = @proxy_pass
if uri.scheme == 'https'
http.use_ssl = true
# 주의: 프로덕션에서는 사용 금지
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http.ssl_version = :TLSv1_2
end
http.request(Net::HTTP::Get.new(uri.request_uri))
end
# 커스텀 CA 번들 사용
def get_with_custom_ca(url, ca_path)
uri = URI.parse(url)
proxy_uri = URI.parse("http://#{PROXY_HOST}:#{PROXY_PORT}")
http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port)
http.proxy_user = @proxy_user
http.proxy_pass = @proxy_pass
if uri.scheme == 'https'
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.ca_file = ca_path
http.cert_store = OpenSSL::X509::Store.new.tap do |store|
store.add_file(ca_path)
store.set_default_paths
end
end
http.request(Net::HTTP::Get.new(uri.request_uri))
end
# 인증서 핀닝 (Certificate Pinning)
def get_with_cert_pinning(url, expected_cert_fingerprint)
uri = URI.parse(url)
proxy_uri = URI.parse("http://#{PROXY_HOST}:#{PROXY_PORT}")
http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port)
http.proxy_user = @proxy_user
http.proxy_pass = @proxy_pass
if uri.scheme == 'https'
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
# 연결 후 인증서 검증
http.start
peer_cert = http.peer_cert
actual_fingerprint = OpenSSL::Digest::SHA256.hexdigest(peer_cert.to_der)
if actual_fingerprint != expected_cert_fingerprint.delete(':').downcase
http.finish
raise SecurityError, "Certificate fingerprint mismatch! Expected: #{expected_cert_fingerprint}, Got: #{actual_fingerprint}"
end
return http.request(Net::HTTP::Get.new(uri.request_uri))
end
http.request(Net::HTTP::Get.new(uri.request_uri))
rescue => e
http&.finish if http&.started?
raise e
end
# Typhoeus로 TLS 설정
def typhoeus_secure_get(url)
response = Typhoeus.get(
url,
proxy: "http://#{@proxy_user}:#{@proxy_pass}@#{PROXY_HOST}:#{PROXY_PORT}",
proxyauth: :basic,
timeout: 30,
ssl_verifypeer: true,
ssl_verifyhost: 2, # 엄격한 호스트 검증
sslversion: :tlsv1_2,
# SSL 세션 캐싱 비활성화 (프록시 환경에서 권장)
ssl_sessionid_cache: false
)
response
end
end
# 사용 예시
client = SecureProxyClient.new('user-country-US', 'your_password')
# 안전한 요청
response = client.secure_get('https://example.com')
puts "Status: #{response.code}"
# 자체 서명 인증서 (내부 API용)
# response = client.insecure_get('https://internal-api.company.local')
# 인증서 핀닝
# fingerprint = 'a1b2c3d4e5f6...' # SHA256 핑거프린트
# response = client.get_with_cert_pinning('https://api.example.com', fingerprint)
Ruby on Rails 통합 가이드
Rails 애플리케이션에서 프록시를 사용할 때는 Faraday 미들웨어, ActiveJob 백그라운드 처리, 설정 관리가 핵심입니다.
Faraday 미들웨어로 프록시 설정
# config/initializers/proxy.rb
require 'faraday'
require 'faraday/retry'
module ProxyHat
class Middleware < Faraday::Middleware
def initialize(app, options = {})
super(app)
@options = options
@proxy_username = ENV['PROXYHAT_USERNAME']
@proxy_password = ENV['PROXYHAT_PASSWORD']
end
def call(env)
# 요청별 동적 프록시 설정
country = env[:request_context]&.[](:country) || 'US'
session = env[:request_context]&.[](:session)
proxy_user = build_username(country, session)
env[:proxy] = "http://#{proxy_user}:#{@proxy_password}@gate.proxyhat.com:8080"
@app.call(env)
end
private
def build_username(country, session = nil)
parts = [@proxy_username, "country-#{country}"]
parts << "session-#{session}" if session
parts.join('-')
end
end
end
# Faraday 연결 설정
module ApiClient
class Connection
def self.create(country: 'US', session: nil)
Faraday.new do |builder|
builder.use ProxyHat::Middleware
builder.request :retry, {
max: 3,
interval: 2,
backoff_factor: 2,
retry_statuses: [429, 503, 502],
methods: [:get, :head, :options]
}
builder.response :json, content_type: /\bjson\b/
builder.response :logger, Rails.logger, bodies: true
builder.adapter Faraday.default_adapter
# TLS 설정
builder.ssl.verify = true
builder.ssl.version = :TLSv1_2
end.tap do |conn|
conn.options.context = { country: country, session: session }
end
end
end
end
# app/jobs/scraping_job.rb
class ScrapingJob < ActiveJob::Base
queue_as :scraping
# 재시도 설정
retry_on Net::ReadTimeout, wait: :exponentially_longer, attempts: 3
retry_on ScraperError, wait: 5.seconds, attempts: 5
discard_on ScraperFatalError
def perform(urls, country: 'US')
@country = country
@results = []
# 병렬 처리를 위한 Hydra 사용
hydra = Typhoeus::Hydra.new(max_concurrency: 20)
urls.each do |url|
request = build_request(url)
request.on_complete { |response| process_response(response, url) }
hydra.queue(request)
end
hydra.run
# 결과 저장
save_results
# 웹훅 전송
notify_webhook if @results.any?
end
private
def build_request(url)
Typhoeus::Request.new(
url,
method: :get,
proxy: proxy_url,
proxyauth: :basic,
timeout: 30,
headers: {
'User-Agent' => user_agent,
'Accept-Language' => accept_language
}
)
end
def proxy_url
username = "#{ENV['PROXYHAT_USERNAME']}-country-#{@country}-session-#{job_id}"
"http://#{username}:#{ENV['PROXYHAT_PASSWORD']}@gate.proxyhat.com:8080"
end
def user_agent
"#{Rails.application.class.module_name.parent}Bot/1.0"
end
def accept_language
case @country
when 'DE' then 'de-DE,de;q=0.9'
when 'JP' then 'ja-JP,ja;q=0.9'
else 'en-US,en;q=0.9'
end
end
def process_response(response, url)
if response.success?
@results << {
url: url,
status: response.code,
body: response.body,
scraped_at: Time.current
}
else
Rails.logger.warn("Scraping failed: #{url} - #{response.code}")
end
end
def save_results
# 벌크 인서트
ScrapedPage.insert_all(@results) if @results.any?
end
def notify_webhook
WebhookService.call(
event: 'scraping.completed',
data: { job_id: job_id, count: @results.size }
)
end
end
# app/services/scraper_service.rb
class ScraperService
def initialize(country: 'US', batch_size: 100)
@country = country
@batch_size = batch_size
end
def scrape(urls)
urls.each_slice(@batch_size).with_index do |batch, index|
ScrapingJob.set(wait: index * 2.seconds).perform_later(batch, @country)
end
end
def scrape_sync(urls)
# 동기 스크래핑 (테스트용)
conn = ApiClient::Connection.create(country: @country)
urls.map do |url|
response = conn.get(url)
{ url: url, status: response.status, body: response.body }
rescue Faraday::Error => e
{ url: url, error: e.message }
end
end
end
# 사용 예시
# 컨트롤러에서
urls = params[:urls].split(',').map(&:strip)
ScraperService.new(country: 'DE').scrape(urls)
Rails 설정 파일
# config/proxyhat.yml
default: &default
username: <%= ENV['PROXYHAT_USERNAME'] %>
password: <%= ENV['PROXYHAT_PASSWORD'] %>
gateway: gate.proxyhat.com
http_port: 8080
socks5_port: 1080
timeout: 30
retries: 3
development:
<<: *default
log_level: debug
test:
<<: *default
mock_mode: true
production:
<<: *default
log_level: warn
circuit_breaker:
threshold: 10
timeout: 60
프록시 유형 비교
| 특성 | 데이터센터 | 레지덴셜 | 모바일 |
|---|---|---|---|
| 속도 | 매우 빠름 | 보통 | 느림 |
| 탐지 위험 | 높음 | 낮음 | 매우 낮음 |
| 가격 | 낮음 | 중간 | 높음 |
| IP 풀 크기 | 작음 | 큼 | 매우 큼 |
| 적합 용도 | 빠른 API 호출 | 웹 스크래핑 | 소셜 미디어 |
Key Takeaways
- Net::HTTP는 표준 라이브러리로 간단한 프록시 요청에 적합합니다.
- Typhoeus + Hydra로 수천 개 URL을 병렬 처리할 수 있습니다.
- ProxyHat SDK로 IP 로테이션, 지역 타겟팅을 쉽게 구현합니다.
- 서킷 브레이커 패턴으로 연속 실패 시 요청을 중단합니다.
- TLS/SSL 검증을 항상 활성화하고, 필요시에만 예외를 허용합니다.
- ActiveJob으로 대량 스크래핑을 백그라운드에서 처리합니다.
Ruby로 프록시를 활용하면 안정적인 대규모 데이터 수집이 가능합니다. ProxyHat 요금제를 확인하고, 전 세계 프록시 위치를 탐색해 보세요. 추가 질문은 블로그에서 더 많은 가이드를 찾을 수 있습니다.






