오류 처리
ActiCrawl은 웹 스크래핑 작업이 안정적이고 신뢰할 수 있도록 포괄적인 오류 처리 메커니즘을 제공합니다. 이 가이드에서는 오류 유형, 처리 전략 및 모범 사례를 다룹니다.
오류 유형
1. 네트워크 오류
네트워크 오류는 대상 웹사이트에 연결하거나 데이터 전송 중에 발생합니다.
# 연결 시간 초과
ActiCrawl::NetworkError::Timeout
# DNS 확인 실패
ActiCrawl::NetworkError::DNSFailure
# 연결 거부됨
ActiCrawl::NetworkError::ConnectionRefused
2. HTTP 오류
HTTP 오류는 대상 서버에서 반환됩니다.
# 404 찾을 수 없음
ActiCrawl::HTTPError::NotFound
# 403 금지됨
ActiCrawl::HTTPError::Forbidden
# 500 내부 서버 오류
ActiCrawl::HTTPError::ServerError
# 429 너무 많은 요청
ActiCrawl::HTTPError::RateLimited
3. 파싱 오류
파싱 오류는 웹 페이지에서 데이터를 추출할 때 발생합니다.
# 잘못된 선택자
ActiCrawl::ParseError::InvalidSelector
# 요소를 찾을 수 없음
ActiCrawl::ParseError::ElementNotFound
# 잘못된 HTML 구조
ActiCrawl::ParseError::MalformedHTML
4. 유효성 검사 오류
유효성 검사 오류는 데이터가 예상 기준을 충족하지 않을 때 발생합니다.
# 필수 필드 누락
ActiCrawl::ValidationError::RequiredFieldMissing
# 잘못된 데이터 형식
ActiCrawl::ValidationError::InvalidFormat
# 범위를 벗어난 데이터
ActiCrawl::ValidationError::OutOfRange
오류 처리 전략
1. 기본 Try-Catch
try-catch 블록을 사용하여 오류를 우아하게 처리합니다:
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. 재시도 로직
일시적인 오류에 대한 지능적인 재시도 메커니즘 구현:
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. 회로 차단기 패턴
연쇄 실패를 방지하기 위한 회로 차단기 구현:
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. 폴백 전략
중요한 작업을 위한 폴백 메커니즘 구현:
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. 로깅
모든 오류에 대한 포괄적인 로깅 구현:
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. 메트릭 수집
오류율과 패턴 추적:
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. 알림
중요한 오류 조건에 대한 알림 설정:
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)
심각한 문제를 나타내는 오류를 숨기지 마세요:
# 나쁨
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. 컨텍스트 제공
오류 로깅 시 관련 컨텍스트 포함:
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. 구체적인 오류 타입 사용
다양한 시나리오에 대한 사용자 정의 오류 클래스 생성:
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. 우아한 성능 저하
기능이 제한된 상태에서도 계속 작동하도록 시스템 설계:
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. 자동 복구
자동 복구 메커니즘 구현:
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. 수동 개입
일부 오류는 수동 개입이 필요합니다:
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. 단위 테스트
격리된 환경에서 오류 처리 테스트:
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. 통합 테스트
실제 시나리오에서 오류 처리 테스트:
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
요약
효과적인 오류 처리는 신뢰할 수 있는 웹 스크래핑 시스템 구축에 필수적입니다. 주요 요점:
- 다양한 유형의 오류를 식별하고 분류하세요
- 일시적 실패에 대한 재시도 로직을 구현하세요
- 연쇄 실패를 방지하기 위해 회로 차단기를 사용하세요
- 오류 패턴을 모니터링하고 알림을 보내세요
- 중요한 작업에 대한 폴백 메커니즘을 제공하세요
- 오류 처리를 철저히 테스트하세요
- 디버깅을 위해 컨텍스트와 함께 오류를 로그하세요
- 우아한 성능 저하를 위해 설계하세요
이러한 패턴과 모범 사례를 따르면 오류를 우아하게 처리하고 높은 가용성을 유지하는 강력한 스크래핑 시스템을 구축할 수 있습니다.