인증, 속도 제한 및 라우팅을 위한 경량 Go API 게이트웨이 구축
Grace Collins
Solutions Engineer · Leapcell

소개
마이크로서비스의 진화하는 환경에서 수많은 자율 서비스를 관리하는 것은 빠르게 복잡해질 수 있습니다. 클라이언트는 종종 여러 서비스와 상호 작용해야 하므로 인증, 요청 스로틀링 및 올바른 서비스 엔드포인트 검색에 어려움이 있습니다. 이때 API 게이트웨이가 매우 유용합니다.
API 게이트웨이는 모든 클라이언트 요청의 단일 진입점 역할을 하며, 공통 관심사를 효과적으로 중앙 집중화하고 클라이언트-서비스 상호 작용을 단순화합니다.
보안, 속도 제한, 서비스 검색과 같은 교차 관심사를 게이트웨이에 오프로드함으로써 개별 마이크로서비스는 핵심 비즈니스 로직에 집중할 수 있습니다.
이 글에서는 Go로 간단하면서도 강력한 API 게이트웨이를 구축하는 방법을 안내하고, 잘 구조화된 마이크로서비스 생태계의 진정한 잠재력을 발휘하는 핵심 기능인 인증, 속도 제한 및 요청 라우팅을 구현하는 방법을 시연합니다.
게이트웨이 필수 사항: 핵심 개념 이해
구현에 들어가기 전에 API 게이트웨이의 기반이 되는 핵심 개념을 정의해 보겠습니다.
- API 게이트웨이: API 요청을 수신하고, 정책(보안 및 할당량 관리 등)을 시행하며, 적절한 백엔드 서비스로 요청을 라우팅하는 API 프런트 엔드 역할을 하는 서버입니다. 마이크로서비스 아키텍처의 복잡성을 클라이언트로부터 추상화합니다.
- 인증: 클라이언트의 신원을 확인하는 프로세스입니다. 여기서는 게이트웨이가 백엔드 서비스로 요청을 계속 진행하기 전에 자격 증명(API 키, JWT 등)을 검증합니다. 이를 통해 인가된 클라이언트만 리소스에 액세스할 수 있습니다.
- 속도 제한: 네트워크 또는 서비스에 대한 들어오거나 나가는 트래픽 양을 제어하는 전략입니다. 남용을 방지하고, 공정한 사용을 보장하며, 과도한 요청으로 백엔드 서비스가 과부하되는 것을 방지합니다. 토큰 버킷 또는 누수 버킷 알고리즘이 일반적인 구현입니다.
- 라우팅: 사전 정의된 규칙에 따라 들어오는 요청을 올바른 백엔드 서비스로 전달하는 프로세스입니다. 이러한 규칙은 일반적으로 URL 경로, HTTP 메서드 또는 기타 요청 헤더를 특정 서비스 엔드포인트와 일치시키는 것을 포함합니다.
이러한 기능들은 API 게이트웨이에 중앙 집중화되면 마이크로서비스 시스템의 관리 용이성, 보안 및 복원력을 크게 향상시킵니다.
게이트웨이 구축: 구현 세부 정보 및 코드 예제
Go API 게이트웨이는 HTTP 요청 처리를 위해 net/http
패키지를, 고급 라우팅 기능을 위해 gorilla/mux
패키지를 활용합니다.
게이트웨이를 인증 및 속도 제한을 위한 별도의 미들웨어와 요청 전달을 위한 핵심 라우터로 구조화할 것입니다.
먼저 프로젝트를 설정하고 기본 main
함수를 정의해 보겠습니다.
package main import ( "log" "net/http" "time" "github.com/gorilla/mux" ) // Main function to initialize the gateway func main() { router := mux.NewRouter() // Register middleware and routes router.Use(LoggingMiddleware) // Basic logging for all requests // Example services (replace with actual service calls) backendService1 := "http://localhost:8081" backendService2 := "http://localhost:8082" // Define routes with middleware publicRoute := router.PathPrefix("/public").Subrouter() publicRoute.HandleFunc("/{path:.*}", NewProxy(backendService1)).Methods("GET") // No auth/rate limit authenticatedRoute := router.PathPrefix("/private").Subrouter() authenticatedRoute.Use(AuthenticationMiddleware) authenticatedRoute.Use(RateLimitingMiddleware) authenticatedRoute.HandleFunc("/{path:.*}", NewProxy(backendService2)).Methods("GET", "POST", "PUT", "DELETE") log.Println("API Gateway listening on :8080") log.Fatal(http.ListenAndServe(":8080", router)) } // LoggingMiddleware logs every incoming request func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() log.Printf("Received request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) next.ServeHTTP(w, r) log.Printf("Completed request: %s %s in %s", r.Method, r.URL.Path, time.Since(start)) }) }
1. 요청 라우팅
NewProxy
함수는 요청을 대상 서비스로 실제로 전달하는 작업을 처리합니다. 이를 위해 httputil.ReverseProxy
를 사용합니다.
package main import ( // ... (existing imports) "net/http/httputil" "net/url" ) // NewProxy creates a ReverseProxy that forwards requests to a target URL func NewProxy(targetURL string) http.HandlerFunc { target, err := url.Parse(targetURL) if err != nil { log.Fatalf("Failed to parse target URL %s: %v", targetURL, err) } proxy := httputil.NewSingleHostReverseProxy(target) // Custom error handler for the proxy proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) { log.Printf("Proxy error for request %s %s: %v", r.Method, r.URL.Path, err) http.Error(rw, "Service temporarily unavailable", http.StatusBadGateway) } return func(w http.ResponseWriter, r *http.Request) { // Modify the request to pass the original path to the backend // This handles cases where the gateway route has a prefix requestPath := mux.Vars(r)["path"] r.URL.Path = "/" + requestPath r.URL.Host = target.Host // Explicitly set host to target host for correct routing log.Printf("Proxying request to %s%s", target.String(), r.URL.Path) proxy.ServeHTTP(w, r) } }
이 라우팅 설정에서 mux.NewRouter()
는 메인 라우터를 생성합니다. 그런 다음 /public
및 /private
에 대한 PathPrefix
경로를 정의합니다. AuthenticationMiddleware
및 RateLimitingMiddleware
는 authenticatedRoute.Use()
를 사용하여 /private
경로에 조건부로 적용되어 특정 라우트 그룹에 미들웨어를 적용하는 방법을 보여줍니다. NewProxy
함수는 각 백엔드 서비스에 대한 리버스 프록시를 동적으로 생성합니다.
2. 인증
인증을 위해 간단한 API 키 유효성 검사를 구현합니다. 실제 시나리오에서는 JWT 유효성 검사 또는 OAuth2와 같은 보다 정교한 메커니즘이 사용됩니다.
package main import ( // ... (existing imports) "net/http" "strings" ) const ( APIKeyHeader = "X-Api-Key" ValidAPIKey = "supersecretapikey" // In a real app, fetch from config/env ) // AuthenticationMiddleware validates the API key in the request header func AuthenticationMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { apiKey := r.Header.Get(APIKeyHeader) if strings.TrimSpace(apiKey) == "" { log.Printf("Authentication failed: Missing %s header from %s", APIKeyHeader, r.RemoteAddr) http.Error(w, "Unauthorized: API Key Missing", http.StatusUnauthorized) return } if apiKey != ValidAPIKey { log.Printf("Authentication failed: Invalid API Key from %s", r.RemoteAddr) http.Error(w, "Unauthorized: Invalid API Key", http.StatusUnauthorized) return } // If authentication is successful, proceed to the next handler log.Printf("Authentication successful for client from %s", r.RemoteAddr) next.ServeHTTP(w, r) }) }
AuthenticationMiddleware
는 X-Api-Key
헤더를 확인하고 미리 정의된 ValidAPIKey
와 비교하여 유효성을 검사합니다. 키가 없거나 유효하지 않으면 401 Unauthorized
상태를 반환합니다. 그렇지 않으면 요청을 체인의 다음 핸들러로 전달합니다.
3. 속도 제한
클라이언트 IP 주소당 간단한 토큰 버킷 속도 제한기를 구현합니다. 프로덕션 환경에서는 Redis와 같은 분산 솔루션을 고려하세요.
package main import ( // ... (existing imports) "sync" "time" ) // RateLimiterConfig defines the rate limiting parameters type RateLimiterConfig struct { MaxRequests int Window time.Duration } // clientBucket represents a token bucket for a specific client type clientBucket struct { tokens int lastRefill time.Time mu sync.Mutex } var ( // In a real application, consider a LRU cache for buckets to prevent unbounded growth clientBuckets = make(map[string]*clientBucket) bucketsMutex sync.Mutex defaultRateConfig = RateLimiterConfig{MaxRequests: 5, Window: 1 * time.Minute} ) // getClientBucket retrieves or creates a token bucket for a client IP func getClientBucket(ip string) *clientBucket { bucketsMutex.Lock() defer bucketsMutex.Unlock() bucket, exists := clientBuckets[ip] if !exists { bucket = &clientBucket{ tokens: defaultRateConfig.MaxRequests, lastRefill: time.Now(), } clientBuckets[ip] = bucket } return bucket } // consumeToken attempts to consume a token from the client's bucket func (b *clientBucket) consumeToken() bool { b.mu.Lock() defer b.mu.Unlock() // Refill tokens based on time elapsed now := time.Now() elapsed := now.Sub(b.lastRefill) refillAmount := int(elapsed.Seconds() / defaultRateConfig.Window.Seconds() * float64(defaultRateConfig.MaxRequests)) if refillAmount > 0 { b.tokens = min(b.tokens+refillAmount, defaultRateConfig.MaxRequests) b.lastRefill = now } if b.tokens > 0 { b.tokens-- return true } return false } func min(a, b int) int { if a < b { return a } return b } // RateLimitingMiddleware enforces rate limits per client IP func RateLimitingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := strings.Split(r.RemoteAddr, ":")[0] // Get client IP from remote address log.Printf("Rate limiting check for IP: %s", ip) bucket := getClientBucket(ip) if !bucket.consumeToken() { log.Printf("Rate limit exceeded for IP: %s", ip) http.Error(w, "Too Many Requests", http.StatusTooManyRequests) return } log.Printf("Rate limit token consumed for IP: %s. Remaining: %d", ip, bucket.tokens) next.ServeHTTP(w, r) }) }
RateLimitingMiddleware
는 기본 토큰 버킷 알고리즘을 구현합니다. 각 클라이언트 IP는 자체 버킷을 갖습니다. 클라이언트가 버킷이 비어 있을 때 요청을 시도하면 429 Too Many Requests
오류가 발생합니다. 토큰은 RateLimiterConfig
에 따라 시간이 지남에 따라 다시 채워집니다.
애플리케이션 시나리오
이 게이트웨이를 테스트하려면 일반적으로 localhost:8081
및 localhost:8082
에서 두 개의 간단한 백엔드 서비스를 실행해야 합니다.
백엔드 서비스 1 (예: 포트 8081의 public-service.go
):
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Printf("Public service received request: %s %s", r.Method, r.URL.Path) fmt.Fprintf(w, "Hello from Public Service! You accessed %s\n", r.URL.Path) }) log.Println("Public Service listening on :8081") log.Fatal(http.ListenAndServe(":8081", nil)) }
백엔드 서비스 2 (예: 포트 8082의 private-service.go
):
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Printf("Private service received request: %s %s", r.Method, r.URL.Path) fmt.Fprintf(w, "Hello from Private Service! You accessed %s\n", r.URL.Path) }) log.Println("Private Service listening on :8082") log.Fatal(http.ListenAndServe(":8082", nil)) }
이 두 서비스를 실행한 다음 게이트웨이를 실행합니다.
http://localhost:8080/public/resource
에 대한 요청은 인증 또는 속도 제한 없이backendService1
로 이동합니다.http://localhost:8080/private/data
에 대한 요청은X-Api-Key: supersecretapikey
헤더가 필요하며 성공적인 유효성 검사 후backendService2
로 전달되는 속도 제한의 적용을 받습니다.
이러한 구조화된 접근 방식은 모듈성을 제공하고 확장이 용이합니다. 로깅, 추적 또는 서킷 브레이커와 같은 더 많은 미들웨어를 쉽게 추가할 수 있습니다.
결론
설명한 대로 Go로 API 게이트웨이를 구축하면 마이크로서비스 상호 작용을 관리하는 강력하고 효율적인 방법을 제공합니다.
인증, 속도 제한, 요청 라우팅과 같은 핵심 기능을 중앙 집중화함으로써 게이트웨이는 클라이언트 측 개발을 단순화하고, 보안을 강화하며, 성능을 개선하고, 분산 시스템의 유지 관리를 용이하게 합니다.
이 접근 방식을 통해 개별 마이크로서비스는 간결함을 유지하고 특정 비즈니스 로직에 집중할 수 있으며, 궁극적으로 더 확장 가능하고 복원력 있는 아키텍처를 만들 수 있습니다.
잘 구현된 API 게이트웨이는 모든 최신 마이크로서비스 배포에 필수적입니다.