Go로 대규모 웹 스크래핑 시스템을 구축하다 보면, 가장 먼저 마주하는 장벽은 IP 차단입니다. 단일 IP에서 수백 건의 요청을 보내면 타겟 서버는 즉시 403이나 429 응답을 반환합니다. Colly 프록시 회전은 이 문제를 해결하는 핵심 기술이며, 이 가이드에서는 Colly의 프록시 인터페이스를 ProxyHat의 회전 프록시와 통합하는 방법을 실전 코드와 함께 다룹니다. golang colly proxy 구성을 처음 접하는 개발자도, 고급 colly web scraping 패턴을 찾는 엔지니어도 모두 활용할 수 있도록 작성했습니다.
법적 주의: 이 글은 공개 데이터 수집에만 적용됩니다. 미국의 CFAA(Computer Fraud and Abuse Act)와 유럽연합의 GDPR은 무단 데이터 접근을 엄격히 금지합니다. robots.txt 준수, 서비스 약관 확인, 그리고 적절한 요청 속도 제한은 선택이 아닌 필수입니다.
Colly 프록시 회전의 기본: 컬렉터 모델 이해하기
Colly는 Go 생태계에서 가장 널리 사용되는 웹 스크래핑 프레임워크입니다. 핵심 설계는 colly.Collector 객체 중심이며, HTTP 요청-응답 라이프사이클을 콜백 체인으로 처리합니다. Colly의 공식 문서(pkg.go.dev)에 따르면, Collector는 다음 주요 콜백을 제공합니다:
- OnRequest(func(*colly.Request)) — 요청 전송 직전 호출. 헤더 수정, 인증 토큰 주입, 프록시 설정에 사용.
- OnHTML(string, func(*colly.HTMLElement)) — CSS 셀렉터로 DOM 요소를 매칭. goquery 기반.
- OnError(func(*colly.Response, error)) — HTTP 에러 발생 시 호출. 재시도 로직 구현에 필수.
- OnResponse(func(*colly.Response)) — 응답 수신 후 호출. 원시 바디 처리.
- OnScraped(func(*colly.Response)) — 모든 OnHTML 콜백 실행 후 호출. 정리 작업용.
Colly는 기본적으로 동기적으로 동작하지만, c.Async = true를 설정하면 비동기 모드로 전환됩니다. 비동기 모드에서는 내부적으로 goroutine을 사용하여 요청을 병렬 처리하며, c.Parallelism으로 동시 요청 수를 제어할 수 있습니다. goquery 기반의 DOM 순회는 jQuery와 유사한 API를 제공하여 Find(), Each(), Attr() 등의 메서드로 요소를 탐색합니다.
프록시 회전이 필요한 이유: 기술적 맥락
현대 웹 서버는 다양한 방어 기제를 사용합니다. Cloudflare, Akamai, PerimeterX 같은 WAF/Web Application Firewall은 IP 평판, 요청 패턴, TLS 핑거프린트를 기반으로 트래픽을 분석합니다. 단일 데이터센터 IP에서 초당 50개 이상의 요청을 보내면 대부분의 서버가 5분 이내에 해당 IP를 차단합니다.
프록시 회전은 각 요청(또는 세션)마다 다른 IP를 사용하여 이러한 차단을 우회합니다. 하지만 모든 프록시가 동등하게 만들어지지는 않습니다:
| 프록시 유형 | IP 출처 | 차단 위험 | 평균 지연 | 적합한 용도 |
|---|---|---|---|---|
| 데이터센터 | AWS, GCP, Azure 등 | 높음 | 50-100ms | 제한 없는 공개 API |
| 레지덴셜 | 실제 ISP 가입자 | 낮음 | 200-500ms | SERP, 이커머스, 소셜 미디어 |
| 모바일 | 4G/5G 통신사 | 매우 낮음 | 300-800ms | 최고 수준의 보안 타겟 |
레지덴셜 프록시는 실제 ISP에서 할당받은 IP를 사용하므로, 타겟 서버가 일반 사용자 트래픽과 스크래퍼 트래픽을 구분하기 매우 어렵습니다. ProxyHat의 레지덴셜 프록시 네트워크는 전 세계 195개국 이상의 IP를 제공합니다. 사용 가능한 위치를 확인하세요.
Colly의 프록시 인터페이스: RoundRobinProxySwitcher와 SetProxyFunc
Colly는 프록시 회전을 위해 두 가지 접근 방식을 제공합니다. colly proxy switcher로서 내장 스위처를 사용하거나, 더 세밀한 제어를 위해 커스텀 함수를 직접 구현할 수 있습니다.
1. proxy.RoundRobinProxySwitcher (내장 스위처)
colly/extensions/proxy 패키지의 RoundRobinProxySwitcher는 프록시 URL 목록을 받아 라운드 로빈 방식으로 순환합니다. 사용이 간단하지만 커스터마이징이 제한적입니다:
package main
import (
"log"
"github.com/gocolly/colly"
"github.com/gocolly/colly/extensions/proxy"
)
func main() {
c := colly.NewCollector()
// ProxyHat 라운드 로빈 프록시 스위처 설정
proxies := []string{
"http://user-country-US:pass@gate.proxyhat.com:8080",
"http://user-country-DE:pass@gate.proxyhat.com:8080",
"http://user-country-GB:pass@gate.proxyhat.com:8080",
"http://user-country-JP:pass@gate.proxyhat.com:8080",
}
if err := proxy.SetProxy(c, proxies...); err != nil {
log.Fatal(err)
}
c.OnHTML("title", func(e *colly.HTMLElement) {
log.Println("Title:", e.Text)
})
c.Visit("https://example.com")
}
proxy.SetProxy는 내부적으로 RoundRobinProxySwitcher를 생성하고 c.SetProxyFunc()에 전달합니다. 프록시 목록이 고정되어 있고 단순한 순환만 필요할 때 적합합니다.
2. c.SetProxyFunc — 커스텀 프록시 함수 (권장)
더 세밀한 제어가 필요한 경우, c.SetProxyFunc(func(*http.Request) (*url.URL, error))를 사용하여 커스텀 프록시 선택 로직을 구현합니다. 이 방식은 요청 컨텍스트, 타겟 URL, 재시도 카운트 등을 기반으로 프록시를 동적으로 선택할 수 있어, golang colly proxy 구성에서 프로덕션 환경에 권장됩니다:
package main
import (
"math/rand"
"net/http"
"net/url"
"sync"
"time"
"github.com/gocolly/colly"
)
type ProxyRotator struct {
proxies []*url.URL
mu sync.Mutex
index int
}
func NewProxyRotator(proxyURLs []string) (*ProxyRotator, error) {
proxies := make([]*url.URL, 0, len(proxyURLs))
for _, p := range proxyURLs {
u, err := url.Parse(p)
if err != nil {
return nil, err
}
proxies = append(proxies, u)
}
return &ProxyRotator{proxies: proxies}, nil
}
// RoundRobin 방식으로 프록시 순환
func (pr *ProxyRotator) GetProxy(r *http.Request) (*url.URL, error) {
pr.mu.Lock()
defer pr.mu.Unlock()
proxy := pr.proxies[pr.index%len(pr.proxies)]
pr.index++
return proxy, nil
}
// 랜덤 선택 방식
func (pr *ProxyRotator) GetRandomProxy(r *http.Request) (*url.URL, error) {
pr.mu.Lock()
defer pr.mu.Unlock()
return pr.proxies[rand.Intn(len(pr.proxies))], nil
}
func main() {
rand.Seed(time.Now().UnixNano())
// ProxyHat 회전 프록시 — 국가별 세션
proxyURLs := []string{
"http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080",
"http://user-country-US-city-newyork:pass@gate.proxyhat.com:8080",
"http://user-country-GB-city-london:pass@gate.proxyhat.com:8080",
"http://user-country-FR-city-paris:pass@gate.proxyhat.com:8080",
}
rotator, err := NewProxyRotator(proxyURLs)
if err != nil {
panic(err)
}
c := colly.NewCollector()
c.SetProxyFunc(rotator.GetProxy)
c.OnHTML("h1", func(e *colly.HTMLElement) {
e.Request.Ctx.Put("h1", e.Text)
})
c.OnScraped(func(r *colly.Response) {
h1 := r.Ctx.Get("h1")
if h1 != "" {
println("Found H1:", h1)
}
})
c.Visit("https://example.com")
}
SetProxyFunc의 장점은 *http.Request 객체에 접근할 수 있다는 것입니다. 이를 통해 요청 URL의 도메인에 따라 다른 프록시를 선택하거나, 재시도 시 다른 국가의 IP로 전환하는 등의 로직을 구현할 수 있습니다.
ProxyHat 회전 프록시 통합: 실전 Go 예제
이제 실제 프로덕션 환경에서 사용할 수 있는 완전한 예제를 살펴보겠습니다. 이 예제는 ProxyHat의 레지덴셜 프록시를 사용하여 여러 국가의 IP를 회전하면서, 요청 속도 제한과 에러 처리를 포함합니다:
package main
import (
"fmt"
"log"
"net/http"
"net/url"
"sync"
"time"
"github.com/gocolly/colly"
)
// ProxyHatRotator는 ProxyHat 게이트웨이를 통해 회전 프록시를 관리합니다.
type ProxyHatRotator struct {
username string
password string
countries []string
mu sync.Mutex
counter int
}
func NewProxyHatRotator(user, pass string, countries []string) *ProxyHatRotator {
return &ProxyHatRotator{
username: user,
password: pass,
countries: countries,
}
}
// ProxyFunc는 Colly의 SetProxyFunc에 전달됩니다.
func (p *ProxyHatRotator) ProxyFunc(r *http.Request) (*url.URL, error) {
p.mu.Lock()
defer p.mu.Unlock()
// 각 요청마다 다른 국가와 세션 ID 사용
country := p.countries[p.counter%len(p.countries)]
sessionID := fmt.Sprintf("sess-%d-%d", time.Now().Unix(), p.counter)
p.counter++
// ProxyHat 사용자명에 국가 및 세션 플래그 포함
proxyStr := fmt.Sprintf(
"http://user-country-%s-session-%s:%s@gate.proxyhat.com:8080",
country, sessionID, p.password,
)
return url.Parse(proxyStr)
}
func main() {
countries := []string{"US", "DE", "GB", "FR", "JP", "CA", "AU"}
rotator := NewProxyHatRotator("user", "pass", countries)
c := colly.NewCollector(
colly.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"),
)
// 커스텀 프록시 함수 설정
c.SetProxyFunc(rotator.ProxyFunc)
// 요청 속도 제한: 도메인당 1초 간격
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Delay: 1 * time.Second,
RandomDelay: 500 * time.Millisecond,
Parallelism: 10,
})
// 비동기 모드 활성화
c.Async = true
// 에러 처리: 재시도 로직
maxRetries := 3
c.OnError(func(r *colly.Response, err error) {
if r.Request.Attempt < maxRetries {
log.Printf("Retry %d/%d for %s: %v",
r.Request.Attempt+1, maxRetries, r.Request.URL, err)
time.Sleep(2 * time.Second)
r.Request.Retry()
} else {
log.Printf("Max retries exceeded for %s", r.Request.URL)
}
})
c.OnHTML("title", func(e *colly.HTMLElement) {
fmt.Printf("[%s] %s -> %s\n",
e.Request.ProxyURL, e.Request.URL, e.Text)
})
// 크롤링할 URL 목록
urls := []string{
"https://example.com",
"https://example.org",
"https://example.net",
}
for _, u := range urls {
c.Visit(u)
}
c.Wait() // 비동기 요청 완료 대기
}
위 예제의 핵심 포인트:
- 세션 ID 회전: 각 요청마다 고유한 세션 ID를 생성하여 다른 IP를 할당받습니다. ProxyHat의
-session-abc123플래그는 동일한 세션 ID에 대해 고정 IP를 보장합니다. 새로운 세션 ID를 전달하면 새로운 IP가 할당됩니다. - 국가별 회전:
-country-DE-city-berlin플래그로 지역 타겟팅을 제어합니다. 이는 SERP 추적과 지역별 가격 모니터링에 필수적입니다. - 속도 제한:
Limit()규칙으로 도메인당 요청 간격을 1초 ± 500ms로 설정합니다.Parallelism: 10은 최대 10개의 동시 요청을 허용합니다. - 재시도:
OnError콜백에서r.Request.Retry()를 호출하여 실패한 요청을 재시도합니다. 최대 3회 재시도 후 포기합니다.
SOCKS5 프록시 사용
일부 환경에서는 SOCKS5 프로토콜이 더 적합할 수 있습니다. ProxyHat의 SOCKS5 엔드포인트는 포트 1080을 사용합니다:
// SOCKS5 프록시 URL 형식
proxyStr := fmt.Sprintf(
"socks5://user-country-%s-session-%s:%s@gate.proxyhat.com:1080",
country, sessionID, password,
)
SOCKS5는 TCP 터널링을 지원하므로, HTTP 프록시가 차단된 네트워크 환경에서 유용합니다. 단, HTTP 프록시에 비해 약간의 성능 오버헤드가 있을 수 있습니다.
프로덕션 패턴: 재시도, 지연, 동시성, 분산
c.Clone()을 활용한 재시도 패턴
단순한 Retry() 외에도, c.Clone()을 사용하여 다른 프록시로 재시도하는 패턴이 유용합니다. 이 방식은 특정 프록시가 차단되었을 때 다른 IP로 즉시 전환할 수 있습니다:
c.OnError(func(r *colly.Response, err error) {
if r.Request.Attempt >= 3 {
log.Printf("Giving up on %s after %d attempts",
r.Request.URL, r.Request.Attempt)
return
}
// 클론 생성 — 새로운 프록시 할당
clone := r.Request.Collector.Clone()
clone.SetProxyFunc(rotator.ProxyFunc)
// 지수 백오프 적용
backoff := time.Duration(r.Request.Attempt) * 2 * time.Second
time.Sleep(backoff)
clone.Visit(r.Request.URL.String())
})
c.Clone()은 원본 Collector의 설정을 복사하지만, 새로운 내부 상태를 가집니다. 이를 통해 원본 Collector의 진행 중인 요청에 영향을 주지 않고, 독립적인 재시도를 수행할 수 있습니다.
RandomDelay와 Parallelism 튜닝
프로덕션 환경에서는 RandomDelay를 활용하여 요청 패턴을 무작위화해야 합니다. 고정된 간격의 요청은 봇 탐지 시스템에 쉽게 감지됩니다. Delay: 2s에 RandomDelay: 1s를 설정하면, 요청 간격이 2-3초 사이의 무작위 값이 됩니다.
Parallelism 값은 타겟 서버의 처리 능력과 프록시 풀 크기에 따라 조정해야 합니다. ProxyHat 레지덴셜 프록시의 경우, 50-100개의 동시 연결이 일반적으로 안정적입니다. 더 높은 동시성이 필요한 경우, ProxyHat 요금제에서 동시 세션 제한을 확인하세요.
커스텀 Transport와 TLS 설정
기본 Go HTTP 클라이언트의 TLS 핑거프린트는 봇 탐지 시스템에 의해 식별될 수 있습니다. http.Transport를 커스터마이징하여 연결 타임아웃, TLS 설정, 커넥션 풀 크기를 조정할 수 있습니다:
import (
"net"
"net/http"
"time"
"github.com/gocolly/colly"
)
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
}
c.WithTransport(transport)
MaxIdleConnsPerHost: 10은 각 타겟 호스트당 최대 10개의 유휴 커넥션을 유지하여, 재연결 오버헤드를 줄입니다. ResponseHeaderTimeout: 15s는 응답 헤더가 15초 내에 도착하지 않으면 타임아웃 처리하여, 응답 없는 프록시로 인한 블로킹을 방지합니다.
분산 스크래핑: Redis 큐 백엔드
단일 머신으로 처리하기 어려운 대규모 크롤링 작업은 분산 아키텍처가 필요합니다. Colly는 queue 패키지와 Redis 스토리지 백엔드를 통해 여러 머신에서 URL 큐를 공유할 수 있습니다:
import (
"github.com/gocolly/colly/queue"
"github.com/gocolly/redisstorage"
)
// Redis 스토리지 설정
storage := &redisstorage.Storage{
Address: "redis-cluster:6379",
Password: "",
Prefix: "proxyhat_crawler",
Client: nil,
}
if err := storage.Init(); err != nil {
panic(err)
}
// 분산 큐 생성
q, _ := queue.New(
2, // 큐 스레드 수
queue.Storage(storage),
)
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Attr("href")
q.AddURL(e.Request.AbsoluteURL(link))
})
// 큐 처리 시작
q.Run(c)
Redis 백엔드를 사용하면 여러 컨테이너 인스턴스가 동일한 큐에서 URL을 소비하여 중복 없이 분산 크롤링을 수행할 수 있습니다. Kubernetes 환경에서는 각 Pod가 독립적인 Colly 인스턴스를 실행하고, Redis 큐를 통해 작업을 조정합니다. Redis 공식 문서에 따르면, 단일 Redis 인스턴스는 초당 100,000건 이상의 읽기/쓰기 작업을 처리할 수 있어, 대규모 분산 스크래핑의 병목이 되지 않습니다.
컨테이너화 패턴
분산 스크래핑을 Docker/Kubernetes 환경에서 운영할 때, 각 컨테이너는 독립적인 프록시 회전 상태를 유지해야 합니다. 다음은 Docker Compose를 사용한 멀티 인스턴스 예제입니다:
# docker-compose.yml
version: '3.8'
services:
scraper:
build: .
deploy:
replicas: 5
environment:
- PROXYHAT_USER=user
- PROXYHAT_PASS=pass
- REDIS_ADDR=redis:6379
- COUNTRIES=US,DE,GB,FR,JP
depends_on:
- redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
5개의 스크래퍼 인스턴스가 각각 다른 국가의 프록시를 사용하여 Redis 큐에서 URL을 소비합니다. 이 구성으로 초당 500건 이상의 요청을 안정적으로 처리할 수 있습니다.
Colly를 사용하면 안 되는 경우
Colly는 HTTP 요청 기반의 스크래핑에 최적화되어 있습니다. 하지만 React, Vue, Angular로 구축된 SPA(Single Page Application)는 JavaScript 실행 없이는 콘텐츠가 렌더링되지 않습니다. Colly는 JavaScript 엔진을 내장하지 않으므로, 이러한 사이트에는 적합하지 않습니다.
JS 렌더링이 필요한 경우:
- chromedp — Go에서 Chrome DevTools Protocol을 사용한 헤드리스 브라우저 자동화
- Playwright — Go 바인딩을 통해 Chromium, Firefox, WebKit 제어
- rod — Go 네이티브 Chrome 자동화 라이브러리
헤드리스 브라우저를 사용할 때도 프록시 회전은 동일하게 필요합니다. 브라우저의 --proxy-server 플래그나 DevTools Protocol의 Network.setExtraHTTPHeaders를 통해 ProxyHat 게이트웨이를 설정할 수 있습니다. 웹 스크래핑 사용 사례에서 브라우저 기반 스크래핑과 프록시 통합에 대해 자세히 알아보세요.
핵심 요약
Colly 프록시 회전 핵심 포인트:
proxy.RoundRobinProxySwitcher는 간단한 회전에 적합하지만,c.SetProxyFunc를 통한 커스텀 함수가 프로덕션에 권장됩니다.- ProxyHat의
-country-XX-city-yy및-session-abc123사용자명 플래그로 지역 타겟팅과 세션 고정을 제어합니다.Limit()규칙으로 요청 속도를 제어하고,RandomDelay로 패턴을 무작위화합니다.c.Clone()을 활용한 재시도는 차단된 프록시에서 새로운 IP로 전환할 수 있습니다.- Redis 큐 백엔드로 여러 인스턴스에서 분산 크롤링을 수행합니다.
- JS 렌더링이 필요한 SPA에는 chromedp, Playwright, rod 같은 헤드리스 브라우저 솔루션을 사용하세요.
- 항상 robots.txt를 준수하고, 공개 데이터만 수집하며, CFAA/GDPR을 존중하세요.
ProxyHat의 레지덴셜 프록시는 Colly와 원활하게 통합되어, SERP 추적, 이커머스 가격 모니터링, 데이터 수집 자동화에 안정적인 인프라를 제공합니다. ProxyHat 공식 문서를 참고하여 더 고급 설정을 확인하고, 요금제에서 프로젝트에 맞는 플랜을 선택하세요.






