마이크로서비스를 위한 강력한 API 게이트웨이 구축
Ethan Miller
Product Engineer · Leapcell

소개: 현대 API의 중추 신경계
빠르게 발전하는 현대 소프트웨어 아키텍처 환경에서 마이크로서비스는 주요 패러다임으로 부상했습니다. 확장성, 유연성 및 독립적인 배포 측면에서 비교할 수 없는 이점을 제공합니다. 그러나 이러한 분산 특성은 복잡성을 야기하기도 합니다. 클라이언트 애플리케이션이 수십 개, 심지어 수백 개의 개별 마이크로서비스와 어떻게 상호 작용합니까? 일관된 보안 정책을 어떻게 보장하고, 서비스 과부하를 방지하며, 네트워크 호출을 최적화합니까? 답은 모든 클라이언트 요청에 대한 단일 진입점 역할을 하는 핵심 구성 요소인 API 게이트웨이에 있습니다. 이 글에서는 이러한 게이트웨이를 구축하는 실질적인 측면을 살펴보고, 인증, 속도 제한 및 요청 집계라는 핵심 책임을 중심으로 클라이언트와 백엔드 생태계 간의 상호 작용을 간소화합니다.
핵심 개념: 게이트웨이의 역할 이해
구현에 앞서 API 게이트웨이의 기능을 뒷받침하는 기본 개념을 정의해 보겠습니다.
- API 게이트웨이: 하나 이상의 API 앞에 배치되어 모든 클라이언트 요청에 대한 단일 진입점 역할을 하는 서버입니다. 내부 시스템 아키텍처를 캡슐화하고 각 클라이언트에 맞는 API를 제공합니다.
- 인증: 사용자 또는 시스템의 신원을 확인하는 프로세스입니다. API 게이트웨이의 맥락에서 이는 종종 토큰(예: JWT)을 검증하여 승인된 엔티티만 다운스트림 서비스에 액세스할 수 있도록 합니다.
- 속도 제한: API 또는 서비스에 액세스하는 속도를 제어하는 데 사용되는 기술입니다. 이는 오용을 방지하고, 서비스 거부 공격으로부터 보호하며, 클라이언트 간의 공정한 사용을 보장합니다.
- 요청 집계: 클라이언트의 여러 요청을 게이트웨이에 대한 단일 API 호출로 결합하는 프로세스로, 게이트웨이는 이 요청을 다양한 내부 서비스로 분배하고 응답을 집계한 다음 클라이언트에게 통합된 응답을 다시 보냅니다. 이를 통해 네트워크 오버헤드와 클라이언트 측 복잡성이 크게 줄어듭니다.
게이트웨이 구축: 아키텍처 및 구현
API 게이트웨이는 클라이언트 애플리케이션과 마이크로서비스 사이에 위치합니다. 일반적으로 프록시 계층, 각 요청에 대한 처리 파이프라인 및 외부 서비스(인증 서버 또는 속도 제한을 위한 캐싱 계층)와 상호 작용하는 메커니즘을 포함합니다.
라우팅 및 미들웨어를 위한 인기 있는 웹 프레임워크인 Gin
을 사용하고 설계 패턴에 대한 영감을 얻기 위해 Kong
또는 Ocelot
개념을 활용하는 Golang 기반 API 게이트웨이를 사용하여 실질적인 예제를 살펴보겠습니다.
인증
게이트웨이는 인증을 적용하는 데 이상적인 장소입니다. 클라이언트가 요청을 보내면 게이트웨이는 이를 가로채 인증 자격 증명(예: JWT를 포함하는 Authorization
헤더)을 추출하여 검증합니다.
원칙: 클라이언트로부터 받은 토큰을 ID 공급자 또는 공유 암호와 대조하여 검증합니다.
구현 예제 (Gin을 사용한 Golang):
package main import ( "log" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/dgrijalva/jwt-go" ) // JWT 검증을 위한 더미 비밀 키 var jwtSecret = []byte("supersecretkey") // AuthMiddleware는 JWT 토큰을 검증합니다 func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) return } tokenString := parts[1] token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.NewValidationError("Unexpected signing method", jwt.ValidationErrorSignatureInvalid) } return jwtSecret, nil }) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token: " + err.Error()}) return } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { c.Set("userID", claims["userID"]) // 다운스트림 핸들러에 사용자 ID 전달 c.Next() } else { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"}) } } } func main() { r := gin.Default() r.Use(AuthMiddleware()) // 모든 라우트에 인증 적용 r.GET("/api/v1/profile", func(c *gin.Context) { userID := c.MustGet("userID").(string) c.JSON(http.StatusOK, gin.H{"message": "Welcome, user " + userID + "!"}) }) // 마이크로서비스로 프록시될 다른 라우트 시뮬레이션 r.GET("/api/v1/products", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": "List of products from service X"}) }) log.Fatal(r.Run(":8080")) // 8080 포트에서 실행 }
이 미들웨어는 요청을 가로채 JWT를 검증하고, 성공하면 사용자 ID를 컨텍스트로 전달하여 다운스트림 서비스에서 사용하거나 로깅할 수 있도록 합니다. 권한 없는 요청은 즉시 거부됩니다.
속도 제한
속도 제한은 백엔드 서비스가 과부하되는 것을 방지하는 데 중요합니다. 고정 창, 슬라이딩 창 또는 토큰 버킷 알고리즘과 같은 다양한 전략을 사용하여 구현할 수 있습니다.
원칙: 특정 클라이언트(IP, API 키 또는 인증된 사용자 ID로 식별)에 대해 시간 창 동안 요청 횟수를 추적하고 사전 정의된 임계값에 도달하면 요청을 거부합니다.
구현 예제 (Gin과 간단한 인메모리 저장소를 사용한 Golang):
package main // ... (기존 import, jwtSecret, AuthMiddleware) ... import ( "sync" time ) // RateLimiter는 각 클라이언트의 요청 수를 저장합니다 type RateLimiter struct { mu sync.Mutex clients map[string]map[int64]int // clientID -> timestamp (window start) -> count limit int // 창당 최대 요청 수 window time.Duration // 시간 창 } // NewRateLimiter는 새 RateLimiter를 생성합니다 func NewRateLimiter(limit int, window time.Duration) *RateLimiter { return &RateLimiter{ clients: make(map[string]map[int64]int), limit: limit, window: window, } } // Allow는 주어진 클라이언트에 대한 요청이 허용되는지 확인합니다 func (rl *RateLimiter) Allow(clientID string) bool { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() currentWindowStart := now.Truncate(rl.window).UnixNano() // 현재 고정된 창 시작 // 이전 창 정리 for ts := range rl.clients[clientID] { if ts < currentWindowStart - rl.window.Nanoseconds() { // 현재 및 이전 창 추적 delete(rl.clients[clientID], ts) } } if _, exists := rl.clients[clientID]; !exists { rl.clients[clientID] = make(map[int64]int) } rl.clients[clientID][currentWindowStart]++ totalRequestsInWindow := 0 for ts, count := range rl.clients[clientID] { // 현재 창 및 잠재적으로 이전 부분 창의 요청을 포함하여 슬라이딩 창 느낌 // 엄격한 고정 창의 경우 currentWindowStart만 확인 if ts == currentWindowStart { // 엄격한 고정 창에 대한 예시 totalRequestsInWindow += count } } return totalRequestsInWindow <= rl.limit } // RateLimitMiddleware는 클라이언트 ID를 기반으로 속도 제한을 적용합니다 func RateLimitMiddleware(rl *RateLimiter) gin.HandlerFunc { return func(c *gin.Context) { // 단순화를 위해 인증된 경우 사용자 ID를 사용하고, 그렇지 않으면 IP 주소를 사용합니다 clientID := c.ClientIP() if val, exists := c.Get("userID"); exists { clientID = val.(string) } if !rl.Allow(clientID) { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "Too many requests"}) return } c.Next() } } // 속도 제한이 있는 메인 함수 func main() { r := gin.Default() // 모든 요청에 대해 전역 속도 제한기 초기화 (예: 10초당 5회 요청) globalRateLimiter := NewRateLimiter(5, 10*time.Second) r.Use(AuthMiddleware(), RateLimitMiddleware(globalRateLimiter)) // 인증을 먼저 적용한 후 속도 제한 적용 r.GET("/api/v1/profile", func(c *gin.Context) { userID := c.MustGet("userID").(string) c.JSON(http.StatusOK, gin.H{"message": "Welcome, user " + userID + "!"}) }) r.GET("/api/v1/products", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": "List of products from service X"}) }) log.Fatal(r.Run(":8080")) // 8080 포트에서 실행 }
참고: 실제 속도 제한기는 특히 클러스터링된 게이트웨이 배포에서 인스턴스 간 동기화 및 성능 향상을 위해 Redis와 같은 분산 저장소를 사용합니다.
요청 집계
클라이언트는 종종 단일 보기를 렌더링하거나 복잡한 작업을 수행하기 위해 여러 서비스의 데이터가 필요합니다. 여러 번의 왕복 대신 게이트웨이가 이러한 요청을 집계할 수 있습니다.
원칙: 게이트웨이는 단일 "복합" 요청을 받으면 이를 다양한 마이크로서비스에 대한 하위 요청으로 분해하고, 이러한 요청을 동시에 실행하며, 응답을 수집한 다음, 단일 응답을 클라이언트에게 다시 구성합니다.
구현 예제 (Golang, 개념적, 특정 /products
및 /users
서비스를 가정):
package main // ... (기존 import, jwtSecret, AuthMiddleware, RateLimiter, 등) ... import ( "encoding/json" "fmt" "io/ioutil" ) // fetchFromService는 내부 서비스에 HTTP 요청을 만드는 헬퍼 함수입니다 func fetchFromService(serviceURL string, client *http.Client) (map[string]interface{}, error) { resp, err := client.Get(serviceURL) if err != nil { return nil, fmt.Errorf("failed to fetch from service %s: %w", serviceURL, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("service %s returned status %d", serviceURL, resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response from service %s: %w", serviceURL, err) } var data map[string]interface{} err = json.Unmarshal(body, &data) if err != nil { return nil, fmt.Errorf("failed to unmarshal JSON from service %s: %w", serviceURL, err) } return data, nil } // AggregateDashboard 핸들러 func AggregateDashboard(c *gin.Context) { // 사용자 프로필과 추천 제품 목록을 // 별도의 마이크로서비스에서 가져오고 싶다고 가정해 보겠습니다. userID := c.MustGet("userID").(string) // AuthMiddleware로부터 var ( profileData map[string]interface{} productsData map[string]interface{} userErr error productErr error wg sync.WaitGroup ) httpClient := &http.Client{Timeout: 5 * time.Second} // 내부 서비스 호출을 위한 클라이언트 wg.Add(1) go func() { defer wg.Done() // 실제 시나리오에서는 내부 프로필 서비스의 URL입니다 profileData, userErr = fetchFromService(fmt.Sprintf("http://localhost:8081/users/%s", userID), httpClient) }() wg.Add(1) go func() { defer wg.Done() // 실제 시나리오에서는 내부 제품 서비스의 URL입니다 productsData, productErr = fetchFromService("http://localhost:8082/recommendations", httpClient) }() wg.Wait() // 모든 고루틴이 완료될 때까지 대기 if userErr != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user profile", "details": userErr.Error()}) return } if productErr != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch product recommendations", "details": productErr.Error()}) return } // 결과 집계 response := gin.H{ "userProfile": profileData, "recommendations": productsData, "status": "success", } c.JSON(http.StatusOK, response) } func main() { r := gin.Default() globalRateLimiter := NewRateLimiter(5, 10*time.Second) r.Use(AuthMiddleware(), RateLimitMiddleware(globalRateLimiter)) r.GET("/api/v1/profile", func(c *gin.Context) { userID := c.MustGet("userID").(string) c.JSON(http.StatusOK, gin.H{"message": "Welcome, user " + userID + "!"}) }) // 집계된 엔드포인트 추가 r.GET("/api/v1/dashboard", AggregateDashboard) log.Fatal(r.Run(":8080")) // 8080 포트에서 실행 }
이 AggregateDashboard
핸들러는 게이트웨이가 어떻게 다른 내부 서비스(/users
및 /recommendations
)로 요청을 분기하고, 이러한 응답을 동시에 기다렸다가, 단일 포괄적인 응답으로 결합하는지를 보여줍니다. 이를 통해 클라이언트에 대한 네트워크 지연 시간과 복잡성이 크게 줄어듭니다.
결론: 현대 마이크로서비스의 기반
인증, 속도 제한 및 요청 집계와 같은 기능을 갖춘 API 게이트웨이를 구현하는 것은 단순한 선택 사항이 아니라 강력하고 확장 가능하며 안전한 마이크로서비스 아키텍처를 구축하기 위한 기본 요구 사항입니다. 이러한 교차 관심사를 중앙 집중화함으로써 API 게이트웨이는 클라이언트 상호 작용을 단순화하고, 시스템 복원력을 향상시키며, 개별 마이크로서비스가 비즈니스 로직에만 집중할 수 있도록 합니다. 진정한 의미에서 분산된 백엔드에 대한 액세스를 보호하고 최적화하는 지능형 프런트 도어 역할을 합니다.