Ruby HTTP 프록시 완벽 가이드: Net::HTTP, Typhoeus, ProxyHat SDK 활용법

Ruby로 HTTP 프록시를 사용하는 방법을 Net::HTTP, Typhoeus, ProxyHat SDK를 통해 알아봅니다. 1000개 URL 동시 스크래핑, TLS/SSL 설정, Rails 통합까지 실전 코드와 함께 제공합니다.

Ruby HTTP 프록시 완벽 가이드: Net::HTTP, Typhoeus, ProxyHat SDK 활용법

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 요금제를 확인하고, 전 세계 프록시 위치를 탐색해 보세요. 추가 질문은 블로그에서 더 많은 가이드를 찾을 수 있습니다.

시작할 준비가 되셨나요?

AI 필터링으로 148개국 이상에서 5천만 개 이상의 레지덴셜 IP에 액세스하세요.

가격 보기레지덴셜 프록시
← 블로그로 돌아가기