문서

문서

ActiCrawl을 사용하여 웹 스크래핑 워크플로를 자동화하는 방법을 알아보세요

오류 처리

ActiCrawl은 웹 스크래핑 작업이 안정적이고 신뢰할 수 있도록 포괄적인 오류 처리 메커니즘을 제공합니다. 이 가이드에서는 오류 유형, 처리 전략 및 모범 사례를 다룹니다.

오류 유형

1. 네트워크 오류

네트워크 오류는 대상 웹사이트에 연결하거나 데이터 전송 중에 발생합니다.

ruby
# 연결 시간 초과
ActiCrawl::NetworkError::Timeout
# DNS 확인 실패
ActiCrawl::NetworkError::DNSFailure
# 연결 거부됨
ActiCrawl::NetworkError::ConnectionRefused

2. HTTP 오류

HTTP 오류는 대상 서버에서 반환됩니다.

ruby
# 404 찾을 수 없음
ActiCrawl::HTTPError::NotFound
# 403 금지됨
ActiCrawl::HTTPError::Forbidden
# 500 내부 서버 오류
ActiCrawl::HTTPError::ServerError
# 429 너무 많은 요청
ActiCrawl::HTTPError::RateLimited

3. 파싱 오류

파싱 오류는 웹 페이지에서 데이터를 추출할 때 발생합니다.

ruby
# 잘못된 선택자
ActiCrawl::ParseError::InvalidSelector
# 요소를 찾을 수 없음
ActiCrawl::ParseError::ElementNotFound
# 잘못된 HTML 구조
ActiCrawl::ParseError::MalformedHTML

4. 유효성 검사 오류

유효성 검사 오류는 데이터가 예상 기준을 충족하지 않을 때 발생합니다.

ruby
# 필수 필드 누락
ActiCrawl::ValidationError::RequiredFieldMissing
# 잘못된 데이터 형식
ActiCrawl::ValidationError::InvalidFormat
# 범위를 벗어난 데이터
ActiCrawl::ValidationError::OutOfRange

오류 처리 전략

1. 기본 Try-Catch

try-catch 블록을 사용하여 오류를 우아하게 처리합니다:

ruby
begin
  result = crawler.fetch("https://example.com/page")
  data = result.extract_data
rescue ActiCrawl::NetworkError => e
  logger.error "네트워크 오류: #{e.message}"
  # 재시도 로직 구현
rescue ActiCrawl::ParseError => e
  logger.error "파싱 오류: #{e.message}"
  # 이 항목 건너뛰기
rescue => e
  logger.error "예상치 못한 오류: #{e.message}"
  # 일반 오류 처리
end

2. 재시도 로직

일시적인 오류에 대한 지능적인 재시도 메커니즘 구현:

ruby
class RetryableRequest
  MAX_RETRIES = 3
  RETRY_DELAY = 5 # 초

  def fetch_with_retry(url)
    retries = 0

    begin
      crawler.fetch(url)
    rescue ActiCrawl::NetworkError, ActiCrawl::HTTPError::ServerError => e
      retries += 1

      if retries <= MAX_RETRIES
        logger.warn "재시도 #{retries}/#{MAX_RETRIES} #{RETRY_DELAY}초 후"
        sleep(RETRY_DELAY * retries)
        retry
      else
        logger.error "#{url}에 대한 최대 재시도 횟수 도달"
        raise
      end
    end
  end
end

3. 회로 차단기 패턴

연쇄 실패를 방지하기 위한 회로 차단기 구현:

ruby
class CircuitBreaker
  FAILURE_THRESHOLD = 5
  TIMEOUT_DURATION = 60 # 초

  def initialize
    @failure_count = 0
    @last_failure_time = nil
    @state = :closed
  end

  def call(url)
    check_state!

    begin
      result = yield
      reset! if @state == :half_open
      result
    rescue => e
      record_failure!
      raise
    end
  end

  private

  def check_state!
    case @state
    when :open
      if Time.now - @last_failure_time > TIMEOUT_DURATION
        @state = :half_open
      else
        raise ActiCrawl::CircuitBreakerOpen
      end
    end
  end

  def record_failure!
    @failure_count += 1
    @last_failure_time = Time.now

    if @failure_count >= FAILURE_THRESHOLD
      @state = :open
    end
  end

  def reset!
    @failure_count = 0
    @last_failure_time = nil
    @state = :closed
  end
end

4. 폴백 전략

중요한 작업을 위한 폴백 메커니즘 구현:

ruby
class DataFetcher
  def fetch_product_data(product_id)
    primary_source_data(product_id)
  rescue ActiCrawl::Error => e
    logger.warn "주 소스 실패: #{e.message}"
    fallback_source_data(product_id)
  rescue => e
    logger.error "모든 소스 실패: #{e.message}"
    cached_data(product_id) || default_data
  end

  private

  def primary_source_data(id)
    crawler.fetch("https://primary.example.com/products/#{id}")
  end

  def fallback_source_data(id)
    crawler.fetch("https://backup.example.com/products/#{id}")
  end

  def cached_data(id)
    cache.get("product:#{id}")
  end

  def default_data
    { status: 'unavailable', message: '데이터를 일시적으로 사용할 수 없습니다' }
  end
end

오류 모니터링

1. 로깅

모든 오류에 대한 포괄적인 로깅 구현:

ruby
class ErrorLogger
  def log_error(error, context = {})
    log_entry = {
      timestamp: Time.now.iso8601,
      error_class: error.class.name,
      message: error.message,
      backtrace: error.backtrace[0..5],
      context: context
    }

    case error
    when ActiCrawl::NetworkError
      logger.error "NETWORK_ERROR: #{log_entry.to_json}"
    when ActiCrawl::ParseError
      logger.warn "PARSE_ERROR: #{log_entry.to_json}"
    else
      logger.error "UNKNOWN_ERROR: #{log_entry.to_json}"
    end
  end
end

2. 메트릭 수집

오류율과 패턴 추적:

ruby
class ErrorMetrics
  def record_error(error_type, url)
    # 오류 카운터 증가
    metrics.increment("errors.#{error_type}")

    # 도메인별 오류율 추적
    domain = URI.parse(url).host
    metrics.increment("errors.by_domain.#{domain}")

    # 실패한 요청의 응답 시간 기록
    metrics.timing("error.response_time", Time.now - @start_time)
  end

  def error_rate(window = 300) # 5분
    total_requests = metrics.get("requests.total", window)
    total_errors = metrics.get("errors.total", window)

    return 0 if total_requests.zero?
    (total_errors.to_f / total_requests * 100).round(2)
  end
end

3. 알림

중요한 오류 조건에 대한 알림 설정:

ruby
class ErrorAlerter
  ALERT_THRESHOLDS = {
    error_rate: 10, # 퍼센트
    consecutive_failures: 5,
    response_time: 30 # 초
  }

  def check_and_alert
    if error_rate > ALERT_THRESHOLDS[:error_rate]
      send_alert("높은 오류율: #{error_rate}%")
    end

    if consecutive_failures > ALERT_THRESHOLDS[:consecutive_failures]
      send_alert("연속적인 실패 감지됨")
    end
  end

  private

  def send_alert(message)
    # 이메일 전송
    AlertMailer.error_alert(message).deliver_later

    # Slack 알림 전송
    slack_notifier.post(text: "🚨 #{message}")

    # 알림 로그
    logger.error "ALERT: #{message}"
  end
end

모범 사례

1. 빠른 실패 (Fail Fast)

심각한 문제를 나타내는 오류를 숨기지 마세요:

ruby
# 나쁨
def fetch_data
  begin
    crawler.fetch(url)
  rescue => e
    nil # 이렇게 하지 마세요!
  end
end

# 좋음
def fetch_data
  begin
    crawler.fetch(url)
  rescue ActiCrawl::NetworkError => e
    logger.error "네트워크 오류: #{e.message}"
    raise # 다시 발생시키거나 적절히 처리
  end
end

2. 컨텍스트 제공

오류 로깅 시 관련 컨텍스트 포함:

ruby
def process_item(item)
  fetch_item_data(item.url)
rescue => e
  logger.error "항목 처리 실패", {
    item_id: item.id,
    url: item.url,
    error: e.message,
    retry_count: item.retry_count
  }
  raise
end

3. 구체적인 오류 타입 사용

다양한 시나리오에 대한 사용자 정의 오류 클래스 생성:

ruby
module ActiCrawl
  class Error < StandardError; end

  class NetworkError < Error
    attr_reader :url, :response_code

    def initialize(message, url: nil, response_code: nil)
      super(message)
      @url = url
      @response_code = response_code
    end
  end

  class RateLimitError < NetworkError
    attr_reader :retry_after

    def initialize(message, retry_after: nil, **kwargs)
      super(message, **kwargs)
      @retry_after = retry_after
    end
  end
end

4. 우아한 성능 저하

기능이 제한된 상태에서도 계속 작동하도록 시스템 설계:

ruby
class ProductScraper
  def scrape_product(url)
    product = {
      title: extract_title(url),
      price: extract_price(url),
      description: extract_description(url),
      images: extract_images(url)
    }

    # 필수 필드 검증
    validate_required_fields!(product)
    product
  end

  private

  def extract_title(url)
    # 중요 필드 - 찾을 수 없으면 오류 발생
    page.at('.product-title')&.text || raise(ParseError, "제목을 찾을 수 없음")
  end

  def extract_price(url)
    # 중요하지만 필수는 아님 - 기본값 사용
    page.at('.price')&.text || "가격 정보 없음"
  end

  def extract_description(url)
    # 선택적 필드 - nil일 수 있음
    page.at('.description')&.text
  rescue => e
    logger.warn "설명 추출 실패: #{e.message}"
    nil
  end
end

오류 복구

1. 자동 복구

자동 복구 메커니즘 구현:

ruby
class AutoRecovery
  def with_recovery(operation, recovery_action = nil)
    operation.call
  rescue => e
    logger.warn "작업 실패, 복구 시도 중: #{e.message}"

    if recovery_action
      recovery_action.call
    else
      default_recovery(e)
    end

    # 복구 후 작업 재시도
    operation.call
  end

  private

  def default_recovery(error)
    case error
    when ActiCrawl::HTTPError::RateLimited
      wait_time = error.retry_after || 60
      logger.info "속도 제한됨, #{wait_time}초 대기"
      sleep(wait_time)
    when ActiCrawl::NetworkError::Timeout
      logger.info "시간 초과 발생, 연결 재설정"
      reset_connection
    end
  end
end

2. 수동 개입

일부 오류는 수동 개입이 필요합니다:

ruby
class ManualInterventionRequired < ActiCrawl::Error
  def initialize(message, action_required:)
    super(message)
    @action_required = action_required
  end

  def notify_operator
    OperatorNotifier.send({
      error: message,
      action_required: @action_required,
      timestamp: Time.now
    })
  end
end

# 사용법
begin
  process_sensitive_data
rescue SecurityError => e
  error = ManualInterventionRequired.new(
    "보안 위반 감지됨",
    action_required: "보안 로그 검토 및 자격 증명 업데이트"
  )
  error.notify_operator
  raise error
end

오류 처리 테스트

1. 단위 테스트

격리된 환경에서 오류 처리 테스트:

ruby
require 'test_helper'

class ErrorHandlingTest < ActiveSupport::TestCase
  def setup
    @scraper = ProductScraper.new
  end

  test "네트워크 시간 초과를 우아하게 처리" do
    stub_request(:get, "https://example.com")
      .to_timeout

    assert_raises(ActiCrawl::NetworkError::Timeout) do
      @scraper.fetch("https://example.com")
    end
  end

  test "일시적 오류에 대한 재시도" do
    stub_request(:get, "https://example.com")
      .to_return(status: 500).times(2)
      .then.to_return(status: 200, body: "성공")

    result = @scraper.fetch_with_retry("https://example.com")
    assert_equal "성공", result.body
  end
end

2. 통합 테스트

실제 시나리오에서 오류 처리 테스트:

ruby
class ErrorHandlingIntegrationTest < ActionDispatch::IntegrationTest
  test "API 오류를 우아하게 처리" do
    # API 오류 시뮬레이션
    mock_api_error(500)

    get "/products/123"

    assert_response :success
    assert_select ".error-message", "제품 데이터를 일시적으로 사용할 수 없습니다"
  end

  test "회로 차단기가 연쇄 실패 방지" do
    # 여러 실패 트리거
    6.times do
      mock_api_error(500)
      get "/products/123"
    end

    # 회로가 열려 있어야 함
    get "/products/124"
    assert_response :service_unavailable
  end
end

요약

효과적인 오류 처리는 신뢰할 수 있는 웹 스크래핑 시스템 구축에 필수적입니다. 주요 요점:

  1. 다양한 유형의 오류를 식별하고 분류하세요
  2. 일시적 실패에 대한 재시도 로직을 구현하세요
  3. 연쇄 실패를 방지하기 위해 회로 차단기를 사용하세요
  4. 오류 패턴을 모니터링하고 알림을 보내세요
  5. 중요한 작업에 대한 폴백 메커니즘을 제공하세요
  6. 오류 처리를 철저히 테스트하세요
  7. 디버깅을 위해 컨텍스트와 함께 오류를 로그하세요
  8. 우아한 성능 저하를 위해 설계하세요

이러한 패턴과 모범 사례를 따르면 오류를 우아하게 처리하고 높은 가용성을 유지하는 강력한 스크래핑 시스템을 구축할 수 있습니다.