Spring Cloud Gateway: Route 완전 정복
Route의 4요소
Route는 Spring Cloud Gateway의 라우팅 단위입니다. 다음 4가지 핵심 요소로 구성됩니다.
| 요소 | 타입 | 설명 |
|---|---|---|
id | String | 라우트 식별자. 유일해야 함. 로그/Actuator에서 참조에 사용 |
uri | URI | 요청을 전달할 목적지. http://, https://, lb:// 지원 |
predicates | List | 요청이 이 라우트에 매칭되는지 판단하는 조건 목록 (AND 조건) |
filters | List | 요청/응답을 가공하는 필터 목록 |
추가 요소:
order: 라우트 우선순위. 낮은 값이 먼저 평가됩니다. 기본값은Integer.MAX_VALUEmetadata: 라우트별 추가 설정 (connect-timeout,response-timeout등)
URI 형태
http:// / https://
직접 호스트를 지정합니다. 로드 밸런싱 없이 고정 주소로 전달합니다.
spring:
cloud:
gateway:
routes:
- id: external-api-route
uri: https://api.example.com
predicates:
- Path=/external/**lb:// — Spring Cloud LoadBalancer 연동
lb://는 서비스 이름으로 인스턴스를 동적으로 조회합니다. Eureka, Kubernetes, Consul 등 서비스 디스커버리와 연동됩니다.
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service # order-service 이름으로 등록된 인스턴스
predicates:
- Path=/api/orders/**ReactiveLoadBalancerClientFilter가 lb://order-service를 실제 http://10.0.0.5:8081과 같은 주소로 변환합니다.
metadata — 라우트별 타임아웃
metadata를 통해 라우트마다 다른 타임아웃을 설정할 수 있습니다. 글로벌 설정보다 우선합니다.
spring:
cloud:
gateway:
# 글로벌 타임아웃
httpclient:
connect-timeout: 3000 # ms
response-timeout: 10s
routes:
# 오래 걸리는 리포트 서비스는 타임아웃을 길게
- id: report-route
uri: lb://report-service
predicates:
- Path=/api/reports/**
metadata:
connect-timeout: 5000 # ms
response-timeout: 60s # Duration 형식
# 결제 서비스는 빠른 타임아웃으로 장애 격리
- id: payment-route
uri: lb://payment-service
predicates:
- Path=/api/payments/**
metadata:
connect-timeout: 1000
response-timeout: 5sorder — 라우트 우선순위
여러 라우트의 Predicate가 동시에 매칭될 경우, order가 낮은 라우트가 먼저 선택됩니다.
spring:
cloud:
gateway:
routes:
# order=1: /api/orders/premium/** 먼저 처리
- id: premium-order-route
uri: lb://premium-order-service
predicates:
- Path=/api/orders/premium/**
order: 1
# order=2: 나머지 /api/orders/** 처리
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
order: 2실무 팁: 구체적인 경로가 더 일반적인 경로보다 먼저 매칭되도록 order를 명시적으로 설정하는 습관을 들이세요. order를 지정하지 않으면 등록 순서에 따라 결정되는데, YAML 순서와 Java DSL 빈 등록 순서가 혼용되면 예상치 못한 동작이 발생합니다.
YAML 방식 전체 예제
spring:
cloud:
gateway:
routes:
- id: user-service-route
uri: lb://user-service
order: 1
predicates:
- Path=/api/v1/users/**
- Method=GET,POST,PUT,DELETE
filters:
- StripPrefix=2 # /api/v1/users/1 → /users/1
- AddRequestHeader=X-Gateway-Source, api-gateway
- AddResponseHeader=X-Response-Source, user-service
metadata:
connect-timeout: 2000
response-timeout: 10sJava DSL 방식 전체 예제
// Java
@Configuration
public class GatewayRoutesConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service-route", r -> r
.path("/api/v1/users/**")
.and()
.method(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)
.filters(f -> f
.stripPrefix(2)
.addRequestHeader("X-Gateway-Source", "api-gateway")
.addResponseHeader("X-Response-Source", "user-service")
)
.metadata("connect-timeout", 2000)
.metadata("response-timeout", "10s")
.order(1)
.uri("lb://user-service")
)
.build();
}
}// Kotlin
@Configuration
class GatewayRoutesConfig {
@Bean
fun routeLocator(builder: RouteLocatorBuilder): RouteLocator =
builder.routes()
.route("user-service-route") { spec ->
spec.path("/api/v1/users/**")
.and()
.method(HttpMethod.GET, HttpMethod.POST)
.filters { f ->
f.stripPrefix(2)
.addRequestHeader("X-Gateway-Source", "api-gateway")
}
.uri("lb://user-service")
}
.build()
}동적 라우트
정적으로 설정 파일에 정의된 라우트 외에도, 런타임에 라우트를 추가/수정/삭제할 수 있습니다.
RouteDefinitionRepository 인터페이스
public interface RouteDefinitionRepository extends RouteDefinitionLocator, RouteDefinitionWriter {
// RouteDefinitionLocator: Flux<RouteDefinition> getRouteDefinitions()
// RouteDefinitionWriter: Mono<Void> save(Mono<RouteDefinition>)
// Mono<Void> delete(Mono<String> routeId)
}RouteDefinition은 YAML에서 라우트 하나를 표현하는 객체입니다:
public class RouteDefinition {
private String id;
private URI uri;
private int order;
private Map<String, Object> metadata;
private List<PredicateDefinition> predicates;
private List<FilterDefinition> filters;
}InMemoryRouteDefinitionRepository (기본 구현)
기본적으로 InMemoryRouteDefinitionRepository가 사용됩니다. 애플리케이션 재시작 시 런타임에 추가한 라우트는 사라집니다.
// 기본 구현 — ConcurrentHashMap에 라우트 저장
@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}Redis 기반 커스텀 RouteDefinitionRepository
재시작 후에도 라우트를 유지하려면 Redis나 DB에 저장하는 커스텀 구현이 필요합니다.
// Java
@Component
@Primary // 기본 InMemory 구현 대신 이 빈 사용
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private static final String ROUTES_KEY = "gateway:routes";
private final ReactiveRedisTemplate<String, RouteDefinition> redisTemplate;
public RedisRouteDefinitionRepository(ReactiveRedisTemplate<String, RouteDefinition> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return redisTemplate.opsForHash()
.values(ROUTES_KEY)
.cast(RouteDefinition.class);
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(r ->
redisTemplate.opsForHash()
.put(ROUTES_KEY, r.getId(), r)
.then()
);
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id ->
redisTemplate.opsForHash()
.remove(ROUTES_KEY, id)
.then()
);
}
}// Kotlin
@Component
@Primary
class RedisRouteDefinitionRepository(
private val redisTemplate: ReactiveRedisTemplate<String, RouteDefinition>
) : RouteDefinitionRepository {
companion object {
private const val ROUTES_KEY = "gateway:routes"
}
override fun getRouteDefinitions(): Flux<RouteDefinition> =
redisTemplate.opsForHash<String, RouteDefinition>()
.values(ROUTES_KEY)
override fun save(route: Mono<RouteDefinition>): Mono<Void> =
route.flatMap { r ->
redisTemplate.opsForHash<String, RouteDefinition>()
.put(ROUTES_KEY, r.id, r)
.then()
}
override fun delete(routeId: Mono<String>): Mono<Void> =
routeId.flatMap { id ->
redisTemplate.opsForHash<String, RouteDefinition>()
.remove(ROUTES_KEY, id)
.then()
}
}Redis 직렬화 설정:
@Configuration
public class RedisConfig {
@Bean
public ReactiveRedisTemplate<String, RouteDefinition> routeDefinitionRedisTemplate(
ReactiveRedisConnectionFactory factory) {
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Jackson2JsonRedisSerializer<RouteDefinition> serializer =
new Jackson2JsonRedisSerializer<>(mapper, RouteDefinition.class);
RedisSerializationContext<String, RouteDefinition> context =
RedisSerializationContext.<String, RouteDefinition>newSerializationContext(
new StringRedisSerializer()
)
.value(serializer)
.hashValue(serializer)
.build();
return new ReactiveRedisTemplate<>(factory, context);
}
}Actuator API로 런타임 라우트 관리
spring-cloud-gateway Actuator 엔드포인트를 통해 런타임에 라우트를 추가/삭제할 수 있습니다.
# application.yml
management:
endpoints:
web:
exposure:
include: gateway# 라우트 추가 (POST)
curl -X POST http://localhost:8080/actuator/gateway/routes/new-route \
-H "Content-Type: application/json" \
-d '{
"id": "new-route",
"uri": "http://localhost:9090",
"predicates": [
{
"name": "Path",
"args": {"_genkey_0": "/new-api/**"}
}
],
"filters": [
{
"name": "StripPrefix",
"args": {"_genkey_0": "1"}
}
]
}'
# 라우트 삭제 (DELETE)
curl -X DELETE http://localhost:8080/actuator/gateway/routes/new-route
# 라우트 캐시 갱신 (POST) — 라우트 변경 후 반드시 호출
curl -X POST http://localhost:8080/actuator/gateway/refresh주의: Actuator의 라우트 CRUD API는
GatewayControllerEndpoint를 통해 동작합니다. 기본InMemoryRouteDefinitionRepository를 사용하면 재시작 시 사라지므로, 영구 저장이 필요하다면 커스텀 Repository를 구현해야 합니다.
RefreshRoutesEvent — 라우트 갱신
코드에서 직접 라우트를 갱신하려면 RefreshRoutesEvent를 발행합니다.
// Java
@Service
public class DynamicRouteService {
private final RouteDefinitionWriter routeDefinitionWriter;
private final ApplicationEventPublisher eventPublisher;
public DynamicRouteService(
RouteDefinitionWriter routeDefinitionWriter,
ApplicationEventPublisher eventPublisher) {
this.routeDefinitionWriter = routeDefinitionWriter;
this.eventPublisher = eventPublisher;
}
public Mono<Void> addRoute(RouteDefinition routeDefinition) {
return routeDefinitionWriter.save(Mono.just(routeDefinition))
.doOnSuccess(v -> {
// 라우트 추가 후 캐시 갱신 이벤트 발행
eventPublisher.publishEvent(new RefreshRoutesEvent(this));
});
}
public Mono<Void> deleteRoute(String routeId) {
return routeDefinitionWriter.delete(Mono.just(routeId))
.doOnSuccess(v -> {
eventPublisher.publishEvent(new RefreshRoutesEvent(this));
});
}
}// Kotlin
@Service
class DynamicRouteService(
private val routeDefinitionWriter: RouteDefinitionWriter,
private val eventPublisher: ApplicationEventPublisher
) {
fun addRoute(routeDefinition: RouteDefinition): Mono<Void> =
routeDefinitionWriter.save(Mono.just(routeDefinition))
.doOnSuccess {
eventPublisher.publishEvent(RefreshRoutesEvent(this))
}
fun deleteRoute(routeId: String): Mono<Void> =
routeDefinitionWriter.delete(Mono.just(routeId))
.doOnSuccess {
eventPublisher.publishEvent(RefreshRoutesEvent(this))
}
}RefreshRoutesEvent를 받으면 CachingRouteLocator가 내부 캐시를 무효화하고 라우트를 다시 로드합니다.
실무 팁
라우트 설계 원칙
- id는 의미 있게:
route-1,route-2보다order-service-route처럼 서비스를 명확히 표현 - order 명시: 모든 라우트에
order를 명시적으로 설정해 예측 가능한 매칭 순서 보장 - metadata 타임아웃 설정: 서비스 SLA에 따라 라우트별 타임아웃을 다르게 설정
- 동적 라우트는 신중하게: 런타임 라우트 변경은 운영 위험이 있으므로, 가능하면 배포로 해결
Spring Cloud Config와 연동
# Spring Cloud Config Server의 gateway-service.yml
spring:
cloud:
gateway:
routes:
- id: order-route
uri: ${ORDER_SERVICE_URL:lb://order-service}
predicates:
- Path=/api/orders/**환경 변수와 Spring Cloud Config를 활용하면 환경별 라우트 설정을 유연하게 관리할 수 있습니다.