API Gateway – MSA의 단일 진입점

핵심 요약

  • API Gateway는 외부 트래픽의 단일 진입점(Single Entry Point)으로, 다수의 마이크로서비스를 “하나의 API”처럼 노출시키는 Edge 컴포넌트입니다.
  • 게이트웨이는 라우팅, 인증/인가, 공통 정책(레이트리밋·CORS·IP 제어), 관측성(Access Log·Trace) 같은 횡단 관심사에 집중해야 하며, 비즈니스 로직이 스며들면 거대한 모놀리식 프론트 도어가 됩니다.
  • 인증을 중앙화하면 서비스마다 토큰 검증/정책 구현이 중복되는 문제를 줄일 수 있지만, 핵심 데이터 접근 같은 최종 인가(Authorization)는 서비스에서도 방어적으로 유지하는 것이 안전합니다.
  • 토폴로지는 Single Shared(단일 공용), BFF(클라이언트별), Hierarchical(엣지/내부 분리)로 나뉘며, 조직 규모/팀 경계/클라이언트 다양성에 따라 “정답”이 달라집니다.
  • 운영 관점에서 게이트웨이는 SPOF가 되기 쉬우므로 반드시 이중화(수평 확장)하고, 홉 증가로 인한 지연(Latency)을 최소화하기 위해 바디 변환·무거운 필터를 제한해야 합니다.
  • 실무에서의 최적 설계는 “얇은 Gateway + (필요하면) BFF 또는 CQRS Read Model” 조합이며, 조합(Aggregation)은 게이트웨이에서 하지 않는 것이 원칙입니다.

목차

게이트웨이는 마이크로서비스를 외부에 노출하는 방식을 단순화하고, 인증/정책/로깅 같은 공통 관심사를 중앙에서 처리하기 위한 매우 강력한 도구입니다. 하지만 게이트웨이에 비즈니스 로직이 들어가기 시작하면, 그 순간부터 게이트웨이는 배포 병목이자 모놀리식 단일 실패 지점이 됩니다. 이 글은 “게이트웨이를 왜 두는지”보다 더 중요한 “게이트웨이를 어디까지 두어야 하는지”를 실무 기준으로 정리합니다.

1. API Gateway란 무엇인가: 왜 MSA에서 자주 등장하는가

API Gateway는 외부 클라이언트(웹/모바일/파트너)가 여러 서비스로 직접 호출하는 대신, 게이트웨이 1곳으로만 들어오도록 강제하는 단일 진입점입니다. 여기서 중요한 포인트는 “단일 진입점” 자체가 목적이 아니라, 단일 진입점을 통해 외부 노출 정책을 통제하고 서비스를 보호하는 것이 목적이라는 점입니다.

클라이언트-투-서비스 호출의 현실적인 문제

  • 엔드포인트 폭발: 서비스가 5개에서 30개로 늘면 클라이언트는 호출 대상/버전/인증 방식을 모두 알아야 합니다.
  • 중복 구현: 각 서비스에 인증/권한/레이트리밋/CORS/공통 헤더 처리를 복붙하는 순간, 변경 비용이 기하급수적으로 늘어납니다.
  • 외부 노출의 위험: 내부 서비스가 직접 인터넷에 노출되면 공격면(Attack Surface)이 증가하고, 네트워크/보안 정책이 산만해집니다.
  • 관측성 분산: 장애가 터졌을 때 “어떤 요청이 어디서부터 실패했는지”를 추적하기가 매우 어려워집니다.

게이트웨이의 범위: Edge(외부) vs 내부 라우팅

실무에서 게이트웨이는 보통 “Edge”에 위치합니다. 즉, 인터넷 트래픽이 처음 통과하는 경계 장치입니다. 반면 서비스 간 통신(동서 트래픽, east-west)은 Service Mesh로 해결하는 경우도 많습니다. 중요한 점은, 게이트웨이는 클라이언트/외부 문제를, 메시/내부 라우터는 서비스-투-서비스 문제를 주로 다룬다는 것입니다. 둘은 역할이 겹칠 수 있지만, 목적과 최적화 포인트가 다릅니다.

핵심 포인트: 게이트웨이는 “서비스를 모아주는 도구”가 아니라, “외부 노출을 통제하고 공통 정책을 일관되게 적용하는 도구”입니다.

2. 핵심 역할: 라우팅·인증·정책·변환·관측성

게이트웨이는 횡단 관심사에 집중해야 합니다. 아래 항목은 실무에서 “게이트웨이에 두면 이득이 큰 것들”이며, 반대로 이 범위를 넘어가면 안티 패턴으로 바로 이어집니다.

역할 무엇을 하는가 실무적 체크포인트
라우팅(Routing) 요청을 어느 서비스로 보낼지 결정(경로/호스트/헤더 기반) 경로 규칙(StripPrefix/Rewrite), 버전 전략(/v1, /v2), 장애 시 폴백 경로
인증/인가(Auth) 토큰 검증, 권한 정책 적용(최소한의 보호선) 서비스별 중복 제거, 정책 중앙화. 단, “최종 인가”는 서비스에도 방어적으로 유지
변환(Transformation) 경로/헤더/바디 변환으로 레거시-신규 연결 바디 변환은 비용 큼. “유예기간” 용도로 제한적으로 사용
공통 정책(Policy) Rate Limit, Throttling, IP 차단, CORS, 헤더 정책 등 정책은 빠르게, 단순하게. 예외 정책(화이트리스트/파트너) 관리 체계 필요
관측성(Observability) Access Log 중앙화, Trace ID 발급/전파, 첫 장애 확인 지점 요청-응답 지연, 라우트 ID, 상태코드, traceId로 “한 요청의 여정”을 재구성

2.1 라우팅: “외부 API”와 “내부 서비스 API”를 분리한다

라우팅은 단순히 프록시가 아닙니다. 게이트웨이는 외부 API의 계약(Contract)을 안정적으로 유지하면서도 내부 서비스의 경로/버전 변경을 흡수해 줍니다. 즉, 외부는 /api/orders/**로 고정해도 내부는 /orders/**, /v2/orders/** 등으로 자유롭게 진화할 수 있습니다.

예시: Path 기반 라우팅 + Prefix 제거

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=2

예시: RewritePath로 정교한 경로 변환

spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/payments/**
          filters:
            - RewritePath=/api/payments/(?<segment>.*), /${segment}

실무 팁: 라우팅 규칙은 “외부 API 계약”이므로, 코드보다 설정 중심으로 관리하는 편이 운영에 유리합니다(변경 범위가 명확해짐).

2.2 인증/인가: 중앙화의 가치와 “경계”

게이트웨이에서 인증을 처리하는 가장 큰 이유는 중복 제거정책 일관성입니다. 서비스마다 JWT 파싱/검증/권한 체크를 구현하면 시간이 지나며 정책이 갈라지고, 취약점도 늘어납니다. 다만 “모든 권한 판단을 게이트웨이에서 끝내겠다”는 접근은 위험할 수 있습니다.

  • 게이트웨이에서 처리하면 좋은 것: 토큰 검증(JWT), 공통 권한 정책(예: /admin은 ADMIN만), API Key 검증, 파트너 화이트리스트
  • 서비스에서도 유지해야 하는 것: 핵심 데이터/행위에 대한 최종 권한 검증(예: 환불, 결제 취소, 정산 같은 고위험 API)

예시: 게이트웨이를 Resource Server로 구성(개념 코드)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
public class SecurityConfig {

  @Bean
  SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http
      .csrf(csrf -> csrf.disable())
      .authorizeExchange(ex -> ex
        .pathMatchers("/actuator/health").permitAll()
        .pathMatchers("/api/admin/**").hasRole("ADMIN")
        .pathMatchers("/api/**").authenticated()
        .anyExchange().denyAll()
      )
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {}))
      .build();
  }
}
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/demo

실무 팁: 인증을 “중앙화”하되, 서비스는 “신뢰하되 검증한다(Trust but verify)” 원칙으로 핵심 API를 방어적으로 설계하는 편이 장애/사고 대응에 강합니다.

2.3 공통 정책: 레이트리밋·IP 제어·CORS는 게이트웨이가 가장 효율적이다

레이트리밋과 IP 정책은 게이트웨이에서 처리하는 것이 자연스럽습니다. 이유는 간단합니다. 정책은 “외부 경계”에서 끊는 것이 가장 싸고 빠르기 때문입니다. 서비스에 들어와서야 제한한다면 이미 리소스를 소비한 뒤입니다.

예시: IP 기준 Rate Limiting(개념 구성)

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/orders/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@ipKeyResolver}"
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

@Configuration
public class RateLimitConfig {

  @Bean
  KeyResolver ipKeyResolver() {
    return exchange -> {
      String ip = exchange.getRequest().getRemoteAddress() != null
        ? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
        : "unknown";
      return Mono.just(ip);
    };
  }
}

예시: CORS 정책(전역)

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          "[/**]":
            allowedOrigins:
              - "https://app.example.com"
            allowedMethods: [GET, POST, PUT, DELETE, OPTIONS]
            allowedHeaders: ["*"]
            allowCredentials: true

주의: 레이트리밋/정책은 “정확도”보다 “안정성”이 더 중요합니다. 정책 구현이 복잡해질수록 게이트웨이는 병목이 되고 장애 포인트가 됩니다.

2.4 관측성: 게이트웨이는 장애 대응의 “첫 번째 화면”이 된다

장애가 발생하면 실무에서 가장 먼저 보는 것은 “서비스 로그”가 아니라 게이트웨이에서의 요청 흐름입니다. 게이트웨이가 아래 정보를 일관되게 남기면, “어디서부터 터졌는지”를 매우 빠르게 좁힐 수 있습니다.

  • 요청 메서드/경로, 라우트 ID, 상태코드
  • 지연시간(ms), 업스트림/다운스트림 타임아웃 여부
  • Trace ID(또는 Correlation ID) 발급/전파 여부

예시: 간단 Access Log(Global Filter, 개념 코드)

import java.time.Duration;
import java.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AccessLogFilterConfig {

  private static final Logger log = LoggerFactory.getLogger("GATEWAY_ACCESS");

  @Bean
  GlobalFilter accessLogFilter() {
    return (exchange, chain) -> {
      Instant start = Instant.now();
      String method = exchange.getRequest().getMethodValue();
      String path = exchange.getRequest().getURI().getRawPath();

      return chain.filter(exchange)
        .doOnSuccess(v -> {
          long ms = Duration.between(start, Instant.now()).toMillis();
          Integer status = exchange.getResponse().getStatusCode() != null
            ? exchange.getResponse().getStatusCode().value()
            : 0;
          log.info("{} {} -> {} ({}ms)", method, path, status, ms);
        })
        .doOnError(e -> {
          long ms = Duration.between(start, Instant.now()).toMillis();
          log.warn("{} {} -> ERR ({}ms) err={}", method, path, ms, e.toString());
        });
    };
  }
}

실무 팁: Trace ID는 “발급”보다 “전파”가 더 중요합니다. 게이트웨이가 헤더로 traceId를 내려주고, 다운스트림도 동일 traceId를 로깅하게 하면, 한 요청의 전체 여정을 재구성할 수 있습니다.

3. 토폴로지(배치 전략): Single Shared vs BFF vs Hierarchical

게이트웨이는 “1개면 끝”이 아닙니다. 서비스 규모/클라이언트 다양성/팀 구조에 따라 토폴로지가 달라집니다. 토폴로지 선택은 결국 “어디에 복잡도를 둘 것인가”의 선택입니다.

3.1 Single Shared(공용 게이트웨이 1개)

Client -> Shared Gateway -> Services
  • 장점: 단순함, 정책 중앙화, 운영·관측성 구축이 빠름
  • 단점: 팀이 늘어나면 라우트/정책 변경 충돌이 잦아지고, “게이트웨이 승인”이 병목이 될 수 있음

3.2 BFF(Backend For Frontend)

Web -> Web BFF -> Services
Mobile -> Mobile BFF -> Services
  • 장점: 클라이언트별 최적 API(필드/응답 형식/버전), 프론트 요구사항 대응이 빠름
  • 단점: 운영 대상 증가, BFF가 사실상 “비즈니스 조합”을 담당하므로 설계·테스트 체계 필요

3.3 Hierarchical(계층형: 엣지/내부 분리)

Client -> Edge Gateway -> Internal Gateway -> Services
  • 장점: 외부 보안/정책과 내부 라우팅을 분리해 책임이 명확해짐
  • 단점: 홉 증가로 지연/장애 포인트 증가, 관측성/트레이싱 체계가 없으면 디버깅 난이도 상승

핵심 포인트: 토폴로지는 “정답”이 아니라 “조직 구조 + 클라이언트 요구 + 운영 역량”의 함수입니다.

4. Anti Patterns: 게이트웨이가 비대해지는 순간 MSA가 무너진다

게이트웨이에 비즈니스 로직이 들어가는 순간, 그 시스템은 MSA의 장점을 잃기 시작합니다. 아래는 실무에서 실제로 자주 터지는 게이트웨이 안티 패턴입니다.

4.1 DB 직접 접근

  • 왜 위험한가: 게이트웨이가 데이터 계층을 갖는 순간 서비스 경계가 붕괴하고, 배포/장애의 영향 범위가 급격히 커집니다.
  • 대안: 조회가 필요하면 해당 서비스의 API를 호출하거나, 조회가 핵심이면 CQRS Read Model을 별도로 둡니다.

4.2 비즈니스 규칙(도메인 로직) 포함

  • 왜 위험한가: 도메인 변경이 게이트웨이 변경으로 연결되며, 모든 팀이 게이트웨이에 의존하게 됩니다.
  • 대안: 도메인 규칙은 “서비스 내부”로. 게이트웨이는 인증/정책/라우팅까지만.

4.3 트랜잭션 처리(분산 트랜잭션의 중심화)

  • 왜 위험한가: 게이트웨이가 트랜잭션 경계를 쥐면 장애 시 복구가 어려워지고, 서비스의 독립 배포가 깨집니다.
  • 대안: 사가(Saga), 아웃박스(Outbox), 이벤트 기반으로 서비스 내부에서 일관성을 구성합니다.

4.4 복잡한 조합(Aggregation) 로직

  • 왜 위험한가: 호출 팬아웃이 늘수록 게이트웨이가 “성능 병목”과 “장애 증폭기”가 됩니다.
  • 대안:
    • 클라이언트별 최적 응답이 목적이면 BFF로 분리합니다.
    • 조합된 조회가 제품 핵심이면 CQRS Read Model로 미리 만들어 둡니다(예: order-query 같은 조회 전용 모델).

핵심 포인트: 게이트웨이는 “흐름을 통제”하는 곳이지, “업무를 처리”하는 곳이 아닙니다.

5. Spring Cloud Gateway 실무 적용: 최소 구성부터 안전한 확장까지

Java 진영에서 대표적인 게이트웨이 구현체는 Spring Cloud Gateway입니다. 실무 관점에서 중요한 것은 “기능을 얼마나 많이 넣을 수 있나”가 아니라, “얼마나 얇게 유지하면서도 운영 요구를 만족시키나”입니다.

5.1 최소 구성 원칙(Thin Gateway)

  • 우선순위 1: 라우팅(경로 정리, 버전 전략)
  • 우선순위 2: 인증/공통 정책(레이트리밋, CORS, IP)
  • 우선순위 3: 관측성(Access Log, traceId, 메트릭)
  • 마지막: 바디 변환/레거시 호환(최소화, 유예기간에만)

5.2 실무적인 라우트 구성 예시(설정 중심)

server:
  port: 8080

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=X-Request-Source, gateway
      routes:
        - id: order-command
          uri: http://localhost:8081
          predicates:
            - Path=/api/order-command/**
          filters:
            - StripPrefix=2

        - id: order-query
          uri: http://localhost:8082
          predicates:
            - Path=/api/order-query/**
          filters:
            - StripPrefix=2

5.3 인증/정책/관측성을 추가할 때의 “실무 안전장치”

  • 인증: 토큰 검증 실패는 빠르게 401/403으로 종료(다운스트림 호출 금지)
  • 정책: 레이트리밋/차단은 “일관된 응답 포맷”을 유지(클라이언트 디버깅 비용 감소)
  • 관측성: 라우트 ID, 상태코드, 지연시간, traceId를 반드시 남김
  • 성능: 바디 변환, 과도한 필터 체인, 동기 블로킹 호출(특히 reactive 환경)을 금지

5.4 (선택) 바디 변환은 “마지막 카드”로만 사용

바디 변환은 레거시 호환에는 유용하지만, 성능과 장애 포인트를 크게 늘립니다. 따라서 장기 전략이 아니라, “계약 정리 전까지의 임시 다리”로만 사용하는 것이 실무적으로 안전합니다.

6. 운영 시 주의사항: SPOF·Latency·장애 대응 루틴

게이트웨이는 운영 난이도를 낮추는 도구이면서도, 동시에 시스템 전체를 멈출 수 있는 위험한 지점이기도 합니다. 따라서 운영 전략이 설계의 일부로 포함되어야 합니다.

6.1 SPOF: 게이트웨이는 반드시 이중화(수평 확장)한다

  • 최소 2개 이상 인스턴스 + 로드밸런서 구성
  • 무상태(Stateless) 유지: 세션을 게이트웨이에 붙이지 말고, 토큰/외부 저장소(예: Redis)로 외부화
  • 무중단 배포: 롤링/블루그린/카나리 등 배포 전략을 전제로 설계(게이트웨이부터 무중단이 되어야 전체가 안정)

6.2 네트워크 홉 증가: Latency 최소화가 최우선

  • 필터는 얇게: 라우팅/인증/정책/관측성 중심, 바디 변환 최소
  • 타임아웃 설계: 다운스트림 지연이 게이트웨이 대기로 누적되면 스레드/이벤트루프가 잠식됨
  • 장애 전파 차단: 서킷 브레이커/백오프/제한 응답으로 “터질 때 같이 터지는” 현상을 줄임

6.3 장애 발생 시 “첫 확인 지점”으로서의 체크리스트

  • 1) 게이트웨이 상태: 5xx 급증? 429 급증? (레이트리밋/정책 오작동 가능)
  • 2) 라우트별 지연: 특정 라우트에서만 지연 증가? (특정 서비스 병목)
  • 3) 인증 실패: 401/403 비율 상승? (인증서/issuer/jwks, 토큰 발급 장애)
  • 4) 트레이싱: 같은 traceId로 다운스트림까지 이어지는지? (전파 단절은 디버깅 난이도를 폭증시킴)

여기까지가 “완성 예제”에 들어가기 전, API Gateway의 개념/역할/토폴로지/안티패턴/운영 포인트를 실무적으로 정리한 내용입니다. 다음 단계에서는 이 원칙을 그대로 적용해서, gateway + order-command + order-query를 하나의 시스템으로 묶고, docker-compose, 라우팅/인증/레이트리밋/트레이싱, 그리고 k6 비교(MySQL 직조회 vs Redis/게이트웨이 경유)까지 “실제로 돌려보며 체감”하는 완성 예제로 넘어가면 됩니다.


이 글은 어떠셨나요? 자유롭게 의견을 남겨주세요! 💬