Go로 네트워크 프록시와 리버스 프록시 구축하기: 실전 가이드

발행: (2026년 1월 12일 오전 10:45 GMT+9)
15 min read
원문: Dev.to

Source: Dev.to

위에 제공된 링크에 있는 전체 텍스트를 여기로 복사해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 현재는 번역할 실제 본문이 제공되지 않았습니다. 텍스트를 공유해 주시면 바로 번역해 드리겠습니다.

왜 Go로 프록시를 구현할까?

Go는 네트워크 프로그래밍에 강력합니다:

FeatureWhy It Rocks for Proxies
net/http packagenet/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

FeatureForward ProxyReverse Proxy
Role클라이언트를 서비스함백엔드를 서비스함
Control클라이언트‑구성서버‑관리
Purpose클라이언트 신원 숨김백엔드 상세 정보 숨김
Go Toolshttp.Clienthttputil.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))
}

실행 방법

  1. 파일을 proxy.go 이름으로 저장합니다.

  2. go run proxy.go 명령을 실행합니다.

  3. 다음과 같이 테스트합니다:

    curl -x http://localhost:8080 http://example.com

동작 원리

  • http.Client가 클라이언트의 요청을 대상 서버로 전달합니다.
  • 원본 요청을 보존하기 위해 헤더와 본문을 복사합니다.
  • io.Copy가 응답을 효율적으로 스트리밍합니다.
  • defer resp.Body.Close()가 파일 디스크립터 누수를 방지합니다.

Pro Tipresp.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)
	}
}

작동 원리

  1. 라운드‑로빈 선택getNextBackend가 원자적으로 다음 백엔드 URL을 선택합니다.
  2. httputil.NewSingleHostReverseProxy – 요청 재작성, 응답 복사, 연결 재사용을 처리합니다.
  3. 커스텀 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과 연계하면 무중단 백엔드 업데이트가 가능합니다.

보안

  • TLShttp.ListenAndServeTLSgolang.org/x/crypto/acme/autocert를 이용해 Let’s Encrypt를 적용하세요.
  • Rate Limitinggolang.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/메모리 정보를 확인합니다.
  • Metricsprometheus/client_golang을 사용해 Prometheus/Grafana 대시보드를 구축합니다.

Pitfall Fix – 메트릭이 없어서 디버깅이 악몽이었습니다. http_requests_total을 추적하도록 Prometheus를 배포하세요.

모범 사례

  • Timeoutshttp.Clienthttp.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/httphttputil은 프록시 개발을 간단하게 만든다.
  • 고루틴과 연결 풀링은 높은 트래픽을 손쉽게 처리한다.
  • 헬스 체크와 TLS와 같은 최적화는 신뢰성을 보장한다.

행동 촉구

직접 프록시를 만들어 보세요! 다음 중 하나를 시도해 보세요:

  • Let’s Encrypt를 사용한 TLS‑지원 포워드 프록시.
  • Prometheus 모니터링이 포함된 리버스 프록시.

댓글에 프로젝트를 공유해 주세요—여러분이 만든 것을 보고 싶어요! 질문이 있거나 문제가 발생했나요? 댓글을 남겨 주세요, 함께 디버깅해 봅시다. 🚀

Back to Blog

관련 글

더 보기 »