들어가며
마이크로서비스 아키텍처(MSA)에서 API Gateway는 클라이언트와 백엔드 서비스 사이의 단일 진입점(Single Entry Point) 역할을 합니다. MSA 아키텍처 완벽 가이드에서 기본 개념 확인하기 라우팅, 인증/인가, Rate Limiting, 로드밸런싱, 요청/응답 변환 등을 담당하며, 클라이언트가 개별 마이크로서비스를 직접 호출하지 않게 합니다.
이 글에서는 Spring Cloud Gateway를 중심으로, 실무에서 필요한 라우팅 설정, 필터 체인, Redis 기반 Rate Limiter, Circuit Breaker 통합, 커스텀 필터 작성까지 심층적으로 다루겠습니다.
API Gateway의 역할
| 역할 | 설명 | 구현 방식 |
|---|---|---|
| 라우팅 | 요청 URL을 적절한 서비스로 전달 | Route Predicate |
| 인증/인가 | JWT 토큰 검증, 권한 체크 | Global Filter |
| Rate Limiting | 과도한 요청 차단 | Redis Rate Limiter |
| 로드밸런싱 | 여러 인스턴스에 요청 분산 | Spring Cloud LoadBalancer |
| Circuit Breaker | 장애 서비스 격리 | Resilience4j |
| 로깅/모니터링 | 요청/응답 로깅, 메트릭 수집 Prometheus + Grafana로 Gateway 메트릭 수집하기 | Gateway Filter |
| 요청/응답 변환 | 헤더 추가, 본문 수정 | Filter |
| CORS | Cross-Origin 정책 관리 | Global CORS Config |
Spring Cloud Gateway 기본 설정
의존성 추가
<!-- build.gradle.kts -->
dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j")
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2024.0.0")
}
}
YAML 기반 라우팅 설정
# application.yml
server:
port: 8080
spring:
cloud:
gateway:
routes:
# 주문 서비스 라우팅
- id: order-service
uri: lb://ORDER-SERVICE # 로드밸런싱
predicates:
- Path=/api/v1/orders/**
- Method=GET,POST,PUT,DELETE
filters:
- StripPrefix=0
- AddRequestHeader=X-Gateway-Source, spring-cloud-gateway
- name: CircuitBreaker
args:
name: orderCircuitBreaker
fallbackUri: forward:/fallback/order
# 상품 서비스 라우팅
- id: product-service
uri: lb://PRODUCT-SERVICE
predicates:
- Path=/api/v1/products/**
filters:
- StripPrefix=0
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@ipKeyResolver}"
# 인증 서비스 라우팅 (Rate Limiting 강화)
- id: auth-service
uri: lb://AUTH-SERVICE
predicates:
- Path=/api/v1/auth/**
filters:
- StripPrefix=0
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 3
redis-rate-limiter.burstCapacity: 5
key-resolver: "#{@ipKeyResolver}"
# 글로벌 CORS 설정
globalcors:
cors-configurations:
'[/**]':
allowed-origins:
- "https://example.com"
allowed-methods:
- GET
- POST
- PUT
- DELETE
allowed-headers: "*"
allow-credentials: true
max-age: 3600
# 기본 필터
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin
Route Predicate 상세
Route Predicate는 요청이 특정 Route에 매칭되는 조건을 정의합니다.
# 다양한 Predicate 조합
spring:
cloud:
gateway:
routes:
- id: advanced-route
uri: lb://MY-SERVICE
predicates:
# 경로 매칭
- Path=/api/v1/users/{userId}
# HTTP 메서드
- Method=GET,POST
# 헤더 조건
- Header=X-Request-Channel, mobile|web
# 쿼리 파라미터
- Query=version, v[2-9]
# 시간 기반 (이벤트 라우팅)
- After=2026-04-01T00:00:00+09:00[Asia/Seoul]
- Before=2026-04-30T23:59:59+09:00[Asia/Seoul]
# 호스트 기반
- Host=api.example.com,api2.example.com
# 쿠키
- Cookie=session_type, premium
Java 코드 기반 라우팅 (프로그래매틱)
@Configuration
public class GatewayRoutesConfig {
@Bean
public RouteLocator customRoutes(RouteLocatorBuilder builder) {
return builder.routes()
// 주문 서비스
.route("order-service", r -> r
.path("/api/v1/orders/**")
.and()
.method(HttpMethod.GET, HttpMethod.POST)
.filters(f -> f
.addRequestHeader("X-Gateway", "true")
.circuitBreaker(cb -> cb
.setName("orderCB")
.setFallbackUri("forward:/fallback/order"))
.retry(retryConfig -> retryConfig
.setRetries(3)
.setStatuses(HttpStatus.SERVICE_UNAVAILABLE)
.setBackoff(Duration.ofMillis(100),
Duration.ofMillis(1000), 2, true)))
.uri("lb://ORDER-SERVICE"))
// 조건부 라우팅: A/B 테스팅
.route("product-v2", r -> r
.path("/api/v1/products/**")
.and()
.header("X-API-Version", "v2")
.filters(f -> f.stripPrefix(0))
.uri("lb://PRODUCT-SERVICE-V2"))
.route("product-v1", r -> r
.path("/api/v1/products/**")
.filters(f -> f.stripPrefix(0))
.uri("lb://PRODUCT-SERVICE-V1"))
.build();
}
}
Redis 기반 Rate Limiter
Rate Limiting은 API Gateway의 가장 중요한 기능 중 하나입니다. Spring Cloud Gateway는 Token Bucket 알고리즘 기반의 Redis Rate Limiter를 내장하고 있습니다. Redis 캐시 전략과 Rate Limiter 활용 패턴
Key Resolver 설정
@Configuration
public class RateLimiterConfig {
// IP 기반 Rate Limiting
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
Objects.requireNonNull(
exchange.getRequest().getRemoteAddress())
.getAddress().getHostAddress());
}
// 사용자 기반 Rate Limiting (JWT에서 추출)
@Bean
public KeyResolver userKeyResolver() {
return exchange -> {
String token = exchange.getRequest().getHeaders()
.getFirst("Authorization");
if (token != null && token.startsWith("Bearer ")) {
String userId = extractUserIdFromToken(
token.substring(7));
return Mono.just(userId);
}
// 미인증 사용자는 IP로 폴백
return Mono.just(exchange.getRequest()
.getRemoteAddress().getAddress().getHostAddress());
};
}
// API 경로 기반 Rate Limiting
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getPath().value());
}
}
커스텀 Rate Limiter 구현
@Component
@RequiredArgsConstructor
public class CustomRedisRateLimiter implements RateLimiter<CustomRedisRateLimiter.Config> {
private final ReactiveRedisTemplate<String, String> redisTemplate;
@Override
public Mono<Response> isAllowed(String routeId, String id) {
String key = "rate_limiter:" + routeId + ":" + id;
String timestampKey = key + ":timestamp";
return redisTemplate.execute(new ReactiveRedisCallback<Response>() {
@Override
public Publisher<Response> doInRedis(
ReactiveRedisConnection connection) {
// Sliding Window Counter 알고리즘
long now = Instant.now().getEpochSecond();
long windowStart = now - 60; // 1분 윈도우
return connection.zSetCommands()
// 윈도우 밖의 오래된 요청 제거
.zRemRangeByScore(toBuffer(key),
Range.closed(0.0, (double) windowStart))
// 현재 요청 추가
.then(connection.zSetCommands()
.zAdd(toBuffer(key),
(double) now,
toBuffer(UUID.randomUUID().toString())))
// 윈도우 내 요청 수 카운트
.then(connection.zSetCommands()
.zCard(toBuffer(key)))
.map(count -> {
boolean allowed = count <= 100; // 분당 100회
Map<String, String> headers = new HashMap<>();
headers.put("X-RateLimit-Remaining",
String.valueOf(Math.max(0, 100 - count)));
headers.put("X-RateLimit-Limit", "100");
headers.put("X-RateLimit-Window", "60s");
return new Response(allowed, headers);
});
}
}).next();
}
@Data
public static class Config {
private int requestsPerMinute = 100;
private int windowSeconds = 60;
}
}
Circuit Breaker 통합 (Resilience4j)
Circuit Breaker는 장애가 발생한 서비스로의 요청을 차단하여 시스템 전체의 안정성을 보호합니다.
설정
# application.yml
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
slidingWindowType: COUNT_BASED
automaticTransitionFromOpenToHalfOpenEnabled: true
instances:
orderCircuitBreaker:
baseConfig: default
failureRateThreshold: 60
waitDurationInOpenState: 30000
paymentCircuitBreaker:
baseConfig: default
failureRateThreshold: 30 # 결제는 더 민감하게
waitDurationInOpenState: 60000
timelimiter:
configs:
default:
timeoutDuration: 3s
instances:
orderCircuitBreaker:
timeoutDuration: 5s
paymentCircuitBreaker:
timeoutDuration: 10s
Fallback Controller
@RestController
@RequestMapping("/fallback")
public class FallbackController {
@GetMapping("/order")
public ResponseEntity<ApiResponse<?>> orderFallback() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ApiResponse.error(
"ORDER_SERVICE_UNAVAILABLE",
"주문 서비스가 일시적으로 불가합니다. 잠시 후 다시 시도해주세요."));
}
@GetMapping("/product")
public ResponseEntity<ApiResponse<?>> productFallback() {
// 캐시된 데이터 반환 가능
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ApiResponse.error(
"PRODUCT_SERVICE_UNAVAILABLE",
"상품 서비스가 일시적으로 불가합니다."));
}
}
커스텀 필터 작성
Global Filter: JWT 인증
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {
private final JwtTokenProvider tokenProvider;
private static final List<String> WHITELIST = List.of(
"/api/v1/auth/login",
"/api/v1/auth/signup",
"/api/v1/products", // 상품 목록 조회는 인증 불필요
"/health",
"/actuator"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
// 화이트리스트 확인
if (isWhitelisted(path)) {
return chain.filter(exchange);
}
// Authorization 헤더 확인
String authHeader = request.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return unauthorized(exchange, "Missing or invalid Authorization header");
}
String token = authHeader.substring(7);
try {
// JWT 검증
Claims claims = tokenProvider.validateAndGetClaims(token);
// 사용자 정보를 헤더에 추가하여 하류 서비스에 전달
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Role", claims.get("role", String.class))
.header("X-User-Email", claims.get("email", String.class))
.build();
return chain.filter(
exchange.mutate().request(modifiedRequest).build());
} catch (ExpiredJwtException e) {
return unauthorized(exchange, "Token expired");
} catch (JwtException e) {
return unauthorized(exchange, "Invalid token");
}
}
private boolean isWhitelisted(String path) {
return WHITELIST.stream().anyMatch(path::startsWith);
}
private Mono<Void> unauthorized(ServerWebExchange exchange,
String message) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders()
.setContentType(MediaType.APPLICATION_JSON);
String body = "{\"error\": \"UNAUTHORIZED\", \"message\": \""
+ message + "\"}";
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return exchange.getResponse().writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 가장 먼저 실행
}
}
Gateway Filter: 요청/응답 로깅
@Component
@Slf4j
public class LoggingGatewayFilterFactory
extends AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {
public LoggingGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String requestId = UUID.randomUUID().toString().substring(0, 8);
long startTime = System.currentTimeMillis();
// 요청 로깅
log.info("[{}] {} {} from {}",
requestId,
request.getMethod(),
request.getPath(),
request.getRemoteAddress());
// 응답 로깅
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
log.info("[{}] Response: {} in {}ms",
requestId, statusCode, duration);
if (duration > config.getSlowRequestThreshold()) {
log.warn("[{}] SLOW REQUEST: {} {} took {}ms",
requestId, request.getMethod(),
request.getPath(), duration);
}
}));
};
}
@Data
public static class Config {
private long slowRequestThreshold = 3000; // 3초 이상이면 경고
}
}
Gateway Filter: 요청 본문 수정
@Component
public class RequestBodyModifyFilter
extends AbstractGatewayFilterFactory<RequestBodyModifyFilter.Config> {
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((exchange, chain) -> {
return ModifyRequestBodyGatewayFilterFactory
.rewriteFunction(exchange, String.class, String.class,
(serverWebExchange, body) -> {
// 요청 본문에 트레이싱 정보 추가
try {
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = (ObjectNode)
mapper.readTree(body);
node.put("gatewayTimestamp",
Instant.now().toString());
node.put("traceId",
UUID.randomUUID().toString());
return Mono.just(
mapper.writeValueAsString(node));
} catch (Exception e) {
return Mono.just(body);
}
})
.filter(exchange, chain);
}, 10);
}
public static class Config {}
}
운영 환경 구성 팁
Actuator 연동
# Gateway 라우트 정보 확인
management:
endpoints:
web:
exposure:
include: gateway, health, metrics, info
endpoint:
gateway:
enabled: true
# GET /actuator/gateway/routes - 전체 라우트 목록 Spring Boot Actuator로 Gateway 운영 모니터링
# GET /actuator/gateway/routes/{id} - 특정 라우트 상세
# POST /actuator/gateway/refresh - 라우트 갱신
Timeout 설정
spring:
cloud:
gateway:
httpclient:
connect-timeout: 2000 # 연결 타임아웃 2초
response-timeout: 5s # 응답 타임아웃 5초
pool:
max-connections: 500 # 최대 연결 수
max-idle-time: 20s # 유휴 연결 유지 시간
max-life-time: 60s # 연결 최대 수명
API Gateway 선택 가이드
| 제품 | 장점 | 단점 | 적합한 경우 |
|---|---|---|---|
| Spring Cloud Gateway | Spring 생태계 통합, Java 개발자 친화적 | 성능은 중간 | Spring 기반 MSA |
| Kong | 플러그인 풍부, 고성능 | Lua 기반 커스터마이징 | 대규모 API 관리 |
| AWS API Gateway | 관리형, 서버리스 통합 | 벤더 종속, 커스터마이징 한계 | AWS 환경 |
| Envoy + Istio | 서비스 메시 통합 | 학습 곡선 높음 | K8s 네이티브 |
| NGINX | 고성능, 안정성 | 동적 설정 제한 | 단순 라우팅/로드밸런싱 |
마치며
API Gateway는 MSA의 관문입니다. 잘 설계된 Gateway는 각 마이크로서비스가 비즈니스 로직에만 집중할 수 있게 하고, 잘못 설계된 Gateway는 전체 시스템의 병목이 됩니다. Spring Cloud Gateway는 리액티브 기반으로 높은 처리량을 제공하면서도, Spring 개발자에게 친숙한 프로그래밍 모델을 제공합니다.
핵심은 Gateway에 너무 많은 책임을 부여하지 않는 것입니다. 헥사고날 아키텍처로 각 서비스 내부 구조화하기 라우팅, 인증, Rate Limiting 정도에 집중하고, 복잡한 비즈니스 로직은 각 서비스에 위임하세요. Gateway가 무거워지면 전체 시스템의 단일 장애점(SPOF)이 되므로, 가볍고 빠르게 유지하는 것이 중요합니다.
'Architecture' 카테고리의 다른 글
| GraphQL 실전 가이드 - REST를 넘어서는 API 설계 (0) | 2026.04.13 |
|---|---|
| 헥사고날 아키텍처 완벽 가이드 - 포트와 어댑터로 클린 아키텍처 구현 (0) | 2026.04.08 |
| DDD(Domain-Driven Design) 실전 가이드 - 전략적 설계부터 전술적 구현까지 (2) | 2026.04.07 |
| MSA 관측 가능성(Observability) 완벽 가이드 (0) | 2026.03.31 |
| MSA 관측 가능성(Observability) 완벽 가이드 (0) | 2026.03.30 |