Go로 네트워크 프록시와 리버스 프록시 구축하기: 실전 가이드
Source: Dev.to
위에 제공된 링크에 있는 전체 텍스트를 여기로 복사해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 현재는 번역할 실제 본문이 제공되지 않았습니다. 텍스트를 공유해 주시면 바로 번역해 드리겠습니다.
왜 Go로 프록시를 구현할까?
Go는 네트워크 프로그래밍에 강력합니다:
| Feature | Why It Rocks for Proxies |
|---|---|
net/http package | net/http 패키지 – HTTP 요청/응답 처리를 간소화 |
| Goroutines | 고루틴 – 동시 요청을 손쉽게 확장 |
| Single binary | 단일 바이너리 – 어디서든 번거로움 없이 배포 |
배울 내용
- 포워드 프록시와 리버스 프록시의 차이점.
- 명확하고 프로덕션‑레디 코드를 사용해 Go에서 두 가지 프록시를 모두 구축하는 방법.
- 10 년간의 Go 경험을 바탕으로 한 최적화 팁과 실제 사례.
함께 시작해 봅시다!
Network Proxies vs. Reverse Proxies: The Basics
코드를 작성하기 전에 프록시가 하는 일을 명확히 하자. 프록시는 네트워크 통신에서 중개자 역할을 하지만, 그 역할은 서로 다릅니다.
Forward Proxy – Your Client’s Advocate
포워드 프록시는 클라이언트(예: 브라우저)와 인터넷 사이에 위치해 클라이언트를 대신해 리소스를 가져옵니다. 마치 당신이 주문한 커피를 대신 가져다 주는 개인 비서와 같습니다.
- How It Works:
Client → Proxy → Server → Proxy → Client - Use Cases: 익명성(VPN), 콘텐츠 필터링, 캐싱
- Go’s Edge:
http.Client가 포워딩을 간단하게 해 주고, Goroutine이 동시에 여러 클라이언트를 처리합니다.
Reverse Proxy – The Server’s Gatekeeper
리버스 프록시는 백엔드 서버 앞에 위치해 클라이언트 요청을 라우팅하고 백엔드를 보호합니다. 마치 레스토랑의 호스트가 주문을 적절한 셰프에게 전달하는 역할과 같습니다.
- How It Works:
Client → Reverse Proxy → Backend → Reverse Proxy → Client - Use Cases: 로드 밸런싱(Nginx), 보안, API 게이트웨이
- Go’s Edge:
httputil.ReverseProxy가 라우팅을 손쉽게 해 주고, Goroutine이 트래픽을 확장합니다.
Forward vs. Reverse – Quick Comparison
| Feature | Forward Proxy | Reverse Proxy |
|---|---|---|
| Role | 클라이언트를 서비스함 | 백엔드를 서비스함 |
| Control | 클라이언트‑구성 | 서버‑관리 |
| Purpose | 클라이언트 신원 숨김 | 백엔드 상세 정보 숨김 |
| Go Tools | http.Client | httputil.ReverseProxy |
Segment 2: Go에서 포워드 프록시 만들기
간단한 HTTP 포워드 프록시를 직접 구현해 봅시다. 이 코드는 클라이언트 요청을 원하는 대상 서버로 전달하고 응답을 반환합니다—익명성 확보나 캐싱에 적합합니다!
Go로 구현하는 간단한 포워드 프록시
package main
import (
"io"
"log"
"net/http"
)
func handleProxy(w http.ResponseWriter, r *http.Request) {
// Create a client that will forward the request
client := &http.Client{}
// Build a new request based on the incoming one
req, err := http.NewRequest(r.Method, r.URL.String(), r.Body)
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
// Copy request headers
for k, v := range r.Header {
req.Header[k] = v
}
// Forward the request
resp, err := client.Do(req)
if err != nil {
http.Error(w, "Server Error", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// Copy response headers and status code
for k, v := range resp.Header {
w.Header()[k] = v
}
w.WriteHeader(resp.StatusCode)
// Stream the response body to the client
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/", handleProxy)
log.Fatal(http.ListenAndServe(":8080", nil))
}
실행 방법
-
파일을
proxy.go이름으로 저장합니다. -
go run proxy.go명령을 실행합니다. -
다음과 같이 테스트합니다:
curl -x http://localhost:8080 http://example.com
동작 원리
http.Client가 클라이언트의 요청을 대상 서버로 전달합니다.- 원본 요청을 보존하기 위해 헤더와 본문을 복사합니다.
io.Copy가 응답을 효율적으로 스트리밍합니다.defer resp.Body.Close()가 파일 디스크립터 누수를 방지합니다.
Pro Tip – resp.Body는 반드시 닫아 주세요. 이를 놓치면 프로덕션 환경에서 파일 디스크립터가 고갈될 수 있습니다.
최적화 트릭
실제 콘텐츠 필터링 프록시에서 다음과 같은 조정으로 성능을 향상시켰습니다:
연결 풀링
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
Timeout: 30 * time.Second,
}
결과: 연결을 재사용함으로써 지연 시간이 약 200 ms에서 약 50 ms로 감소했습니다.
함정 해결 – MaxIdleConnsPerHost를 설정하여 호스트당 연결 수를 제한하고 과도한 TCP 핸드셰이크를 방지합니다.
Source:
Segment 3: Go에서 역방향 프록시 만들기
이제 라운드‑로빈 로드 밸런싱을 사용해 여러 백엔드에 요청을 분산시키는 역방향 프록시를 만들어 보겠습니다. 마이크로서비스나 트래픽이 많은 애플리케이션에 이상적입니다!
라운드‑로빈을 이용한 간단한 역방향 프록시
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"sync/atomic"
)
// ReverseProxy holds the backend URLs and the current index.
type ReverseProxy struct {
backends []*url.URL
current uint64
}
// NewReverseProxy parses the backend URLs and returns a proxy instance.
func NewReverseProxy(backendURLs []string) *ReverseProxy {
urls := make([]*url.URL, len(backendURLs))
for i, u := range backendURLs {
parsed, err := url.Parse(u)
if err != nil {
log.Fatalf("Invalid backend URL %q: %v", u, err)
}
urls[i] = parsed
}
return &ReverseProxy{backends: urls}
}
// getNextBackend returns the next backend URL using atomic round‑robin.
func (p *ReverseProxy) getNextBackend() *url.URL {
idx := atomic.AddUint64(&p.current, 1)
return p.backends[idx%uint64(len(p.backends))]
}
// ServeHTTP satisfies http.Handler and forwards the request.
func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
target := p.getNextBackend()
// Create a reverse proxy for the selected backend.
proxy := httputil.NewSingleHostReverseProxy(target)
// Optionally modify the request before sending it upstream.
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
// Preserve the original Host header if you need it.
req.Host = target.Host
}
proxy.ServeHTTP(w, r)
}
func main() {
// Example backends – replace with your own services.
backends := []string{
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:8083",
}
proxy := NewReverseProxy(backends)
log.Println("Reverse proxy listening on :8080")
if err := http.ListenAndServe(":8080", proxy); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
작동 원리
- 라운드‑로빈 선택 –
getNextBackend가 원자적으로 다음 백엔드 URL을 선택합니다. httputil.NewSingleHostReverseProxy– 요청 재작성, 응답 복사, 연결 재사용을 처리합니다.- 커스텀 Director – 외부 요청을 조정할 수 있습니다(예: 원본
Host헤더 유지).
테스트 방법
# 세 개의 간단한 백엔드 서버를 시작합니다(예: Python의 http.server 사용)
python3 -m http.server 8081 &
python3 -m http.server 8082 &
python3 -m http.server 8083 &
# 역방향 프록시 실행
go run reverse_proxy.go
# 요청 전송
curl http://localhost:8080
curl http://localhost:8080
curl http://localhost:8080
세 백엔드가 차례대로 요청을 처리하는 것을 확인할 수 있습니다.
프로 팁 – 프로덕션 환경에서는 헬스 체크, 정상 종료, TLS 종료 등을 추가하는 것을 고려하세요.
마무리
프록시는 Go의 네트워킹 스택을 마스터하기 위한 훌륭한 놀이터입니다. 포워드‑프록시 예제를 통해 이제 익명성이나 콘텐츠 필터링을 위한 기반을 마련했으며, 라운드‑로빈 로드 밸런싱을 갖춘 리버스‑프록시는 경량 API 게이트웨이 또는 엣지 라우터를 구축하는 방법을 보여줍니다.
스니펫을 자유롭게 포크하고, TLS를 실험해 보며, 캐시 계층을 추가하거나 메트릭(Prometheus, OpenTelemetry)을 통합해 보세요. 즐거운 코딩 되시고, 여러분의 Go 루틴이 절대 블록되지 않기를 바랍니다! 🚀
Segment 3: Go에서 간단한 리버스 프록시
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"sync/atomic"
)
type ReverseProxy struct {
backends []*url.URL
counter uint64
}
func NewReverseProxy(backends []string) *ReverseProxy {
urls := make([]*url.URL, len(backends))
for i, b := range backends {
u, err := url.Parse(b)
if err != nil {
log.Fatalf("invalid backend URL %s: %v", b, err)
}
urls[i] = u
}
return &ReverseProxy{backends: urls}
}
func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Round‑robin selection
index := atomic.AddUint64(&p.counter, 1) % uint64(len(p.backends))
proxy := httputil.NewSingleHostReverseProxy(p.backends[index])
proxy.ServeHTTP(w, r)
}
func main() {
backends := []string{"http://localhost:8081", "http://localhost:8082"}
proxy := NewReverseProxy(backends)
log.Fatal(http.ListenAndServe(":8080", proxy))
}
실행 방법
- 파일을
reverse_proxy.go로 저장합니다. - 포트 8081 및 8082에서 모의 백엔드(예: 간단한 Go HTTP 서버)를 실행합니다.
go run reverse_proxy.go를 실행합니다.curl http://localhost:8080로 테스트합니다.
무슨 일이 일어나고 있나요
httputil.ReverseProxy가 요청 포워딩을 처리합니다.atomic.AddUint64로 스레드‑안전한 라운드‑로빈 선택을 보장하므로 요청이 백엔드 사이에서 교대로 전달됩니다.
팁 – 연결 재사용 활성화:
proxy.Transport = &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
}
실제 적용 인사이트
헬스 체크
func (p *ReverseProxy) healthCheck() {
for {
for _, backend := range p.backends {
resp, err := http.Get(backend.String() + "/health")
// Update backend status based on err / resp.StatusCode
if resp != nil {
resp.Body.Close()
}
}
time.Sleep(10 * time.Second)
}
}
함정 해결
느린 백엔드 지연을 방지하려면 ResponseHeaderTimeout 을 설정합니다.
Source: …
고급 기능 및 모범 사례
고급 기능인 동시성, 보안, 모니터링을 활용하고, 프록시를 프로덕션 수준으로 만들기 위한 모범 사례를 살펴보세요.
높은 동시성
Go의 goroutine이 여기서 빛을 발합니다. 각 요청이 자체 경량 스레드에서 실행되어 최소 메모리로 수천 개의 연결을 처리합니다. 동적 백엔드의 경우, 스레드‑안전 매니저를 사용하세요:
type BackendManager struct {
backends []*url.URL
mu sync.RWMutex
}
func (m *BackendManager) UpdateBackends(newBackends []string) {
m.mu.Lock()
defer m.mu.Unlock()
urls := make([]*url.URL, len(newBackends))
for i, u := range newBackends {
urls[i], _ = url.Parse(u)
}
m.backends = urls
}
Insight – Kubernetes 환경에서 Consul과 연계하면 무중단 백엔드 업데이트가 가능합니다.
보안
- TLS –
http.ListenAndServeTLS와golang.org/x/crypto/acme/autocert를 이용해 Let’s Encrypt를 적용하세요. - Rate Limiting –
golang.org/x/time/rate로 DDoS를 완화합니다:
limiter := rate.NewLimiter(10, 50) // 초당 10 요청, 버스트 50
if !limiter.Allow() {
http.Error(w, "Rate Limit Exceeded", http.StatusTooManyRequests)
return
}
모니터링
- Profiling –
:6060포트에net/http/pprof를 활성화해 CPU/메모리 정보를 확인합니다. - Metrics –
prometheus/client_golang을 사용해 Prometheus/Grafana 대시보드를 구축합니다.
Pitfall Fix – 메트릭이 없어서 디버깅이 악몽이었습니다. http_requests_total을 추적하도록 Prometheus를 배포하세요.
모범 사례
- Timeouts –
http.Client와http.Transport에 타임아웃을 설정해 무한 대기를 방지합니다. - Logging – 구조화되고 성능이 뛰어난 로그를 위해
go.uber.org/zap을 사용합니다. - Deployment – Docker로 컨테이너화합니다:
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o proxy
CMD ["./proxy"]
Insight – Nginx + Go 조합으로 SSL 종료를 담당하면 성능이 약 20 % 향상됩니다.
실제 사용 사례 및 행동 촉구
API 게이트웨이
인증을 포함한 마이크로서비스로 요청 라우팅:
mux := http.NewServeMux()
mux.Handle("/users/", httputil.NewSingleHostReverseProxy(userService))
mux.Handle("/orders/", httputil.NewSingleHostReverseProxy(orderService))
팁 – 라우팅 충돌을 방지하려면 http.StripPrefix를 사용하세요.
로드 밸런서
일관된 해싱(github.com/stathat/consistent)을 사용해 캐시 적중률을 높이고 동적 헬스 체크를 수행합니다.
캐싱 프록시
sync.Map과 TTL을 이용해 정적 콘텐츠를 캐시:
type CacheEntry struct {
Data []byte
ExpiresAt time.Time
}
p.cache.Store(key, CacheEntry{
Data: data,
ExpiresAt: time.Now().Add(5 * time.Minute),
})
인사이트 – 이를 통해 캐시 적중률이 60 %에서 85 %로 상승했습니다.
핵심 요점
- Go의
net/http와httputil은 프록시 개발을 간단하게 만든다. - 고루틴과 연결 풀링은 높은 트래픽을 손쉽게 처리한다.
- 헬스 체크와 TLS와 같은 최적화는 신뢰성을 보장한다.
행동 촉구
직접 프록시를 만들어 보세요! 다음 중 하나를 시도해 보세요:
- Let’s Encrypt를 사용한 TLS‑지원 포워드 프록시.
- Prometheus 모니터링이 포함된 리버스 프록시.
댓글에 프로젝트를 공유해 주세요—여러분이 만든 것을 보고 싶어요! 질문이 있거나 문제가 발생했나요? 댓글을 남겨 주세요, 함께 디버깅해 봅시다. 🚀