Spring gateway을 사용할 때 일반적으로 2가지 방식으로 route 설정을 할 수 있는데 어떤게 더 좋을까?
모든 소스는 이곳에서 확인 가능합니다:)
Spring Gateway Route
Spring gateway을 사용할 때 일반적으로 2가지 방식으로 route 설정을 할 수 있다.
Yaml
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: '*'
allowedMethods:
- POST
- GET
- PUT
- OPTIONS
- DELETE
routes:
- id: a-service
uri: http://localhost:18081
predicates:
- Path=/aservice/**
- id: b-service
uri: http://localhost:18082
predicates:
- Path=/bservice/**
2022-02-19 18:07:46.827 DEBUG 33218 --- [ restartedMain] o.s.c.gateway.config.GatewayProperties : Routes supplied from Gateway Properties: [RouteDefinition{id='a-service', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/aservice/**}}], filters=[], uri=http://localhost:18081, order=0, metadata={}}, RouteDefinition{id='b-service', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/bservice/**}}], filters=[], uri=http://localhost:18082, order=0, metadata={}}]
Java
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RouteConfig {
@Bean
public RouteLocator bRoue(RouteLocatorBuilder builder) {
return builder.routes()
.route("q-service", r -> r.path("/q-service/**")
.uri("http://localhost:18081"))
.route("q-service", r -> r.path("/w-service/**")
.uri("http://localhost:18080"))
.build();
}
@Bean
public RouteLocator cRoute(RouteLocatorBuilder builder) {
return builder.routes()
.route("c-service", r -> r.path("/c-service/**")
.uri("http://localhost:18081"))
.route("d-service", r -> r.path("/d-service/**")
.uri("http://localhost:18080"))
.build();
}
}
라우트들을 나눠서 설정할 수 있다. 그렇다면 따로 설정한 라우터들이 어떻게 같이 합쳐져서 작동하는 것 일까?
Route 구성도
https://zhuanlan.zhihu.com/p/359523303
일단 먼저 봐야할 것은 RouteDefinationLocator이다.
RouteDefinationLocator
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}
RouteDefinationLocator을 구현한 객체들은 5개가 있다.
CachingRouteDefinitionLocatorCompositeRouteDefinitionLocatorDiscoveryClientRouteDefinitionLocatorPropertiesRouteDefinitionLocatorRouteDefinitionRepository
DiscoveryClientRouteDefinitionLocator
Service Discovery(Netflix Eureka, Consul, or Zookeeper)와 연동하여 등록된 서비스들을 기준으로 경로를 만든다.
PropertiesRouteDefinitionLocator
Yaml에 설정한 property value들이 GateWayProperty 직렬화되어 값이 들어가고
public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator {
private final GatewayProperties properties;
public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
this.properties = properties;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromIterable(this.properties.getRoutes());
}
}

GatewayProperty 값은 PropertiesRouteDefinitionLocator 에 쓰이게 된다.

CompositeRouteDefinitionLocator
자바 DSL(편리하게 fluent API를 지원)로 설정한 라우터들의 값이 들어가 있고
CompositeRouteDefinitionLocator에서 PropertiesRouteDefinitionLocator에서 설정한 라우터들에 id가 없을 경우 randid가 들어가게 된다.
public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {
...
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return this.delegates.flatMapSequential(RouteDefinitionLocator::getRouteDefinitions)
.flatMap(routeDefinition -> {
if (routeDefinition.getId() == null) {
return randomId().map(id -> {
routeDefinition.setId(id);
if (log.isDebugEnabled()) {
log.debug("Id set on route definition: " + routeDefinition);
}
return routeDefinition;
});
}
return Mono.just(routeDefinition);
});
}
}
RouteDefinitionRouteLocator
Route들을 key를 기준으로 확인 후에 RoutePredicateFactory에 집어넣는다.
public class RouteDefinitionRouteLocator implements RouteLocator {
...
public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
List<RoutePredicateFactory> predicates, List<GatewayFilterFactory> gatewayFilterFactories,
GatewayProperties gatewayProperties, ConfigurationService configurationService) {
this.routeDefinitionLocator = routeDefinitionLocator;
this.configurationService = configurationService;
initFactories(predicates);
gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
this.gatewayProperties = gatewayProperties;
}
// RoutePredicateFactory 검사
private void initFactories(List<RoutePredicateFactory> predicates) {
predicates.forEach(factory -> {
String key = factory.name();
if (this.predicates.containsKey(key)) {
this.logger.warn("A RoutePredicateFactory named " + key + " already exists, class: "
+ this.predicates.get(key) + ". It will be overwritten.");
}
this.predicates.put(key, factory);
if (logger.isInfoEnabled()) {
logger.info("Loaded RoutePredicateFactory [" + key + "]");
}
});
}
public Flux<Route> getRoutes() {
Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
if (!gatewayProperties.isFailOnRouteDefinitionError()) {
// instead of letting error bubble up, continue
routes = routes.onErrorContinue((error, obj) -> {
if (logger.isWarnEnabled()) {
logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId()
+ " will be ignored. Definition has invalid configs, " + error.getMessage());
}
});
}
return routes.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});
}
...
}
CompositeRouteLocator
여러 개의 RouteLocator 들은 delegates를 가지고, 각 RouteLocator들이 가지고 있는 Route들을 하나로 합친다.
public class CompositeRouteLocator implements RouteLocator {
private final Flux<RouteLocator> delegates;
public CompositeRouteLocator(Flux<RouteLocator> delegates) {
this.delegates = delegates;
}
@Override
public Flux<Route> getRoutes() {
return this.delegates.flatMapSequential(RouteLocator::getRoutes);
}

this.deligates 안에 설정한 route들이 담겨있는 것을 볼 수 있다.
CachingRouteLocator
deletgate들을 받아 캐시에 저장해서 route들을 가진다.
public class CachingRouteLocator implements Ordered, RouteLocator, ApplicationListener<RefreshRoutesEvent>, ApplicationEventPublisherAware {
private final Map<String, List> cache = new ConcurrentHashMap<>();
public CachingRouteLocator(RouteLocator delegate) {
this.delegate = delegate;
routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class).onCacheMissResume(this::fetch);
}
private Flux<Route> fetch() {
return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);
}
}
Cache에 Route들이 담기게 된다.

그리고 마지막으로 최초 시작점인 GatewayAutoConfiguration
Gateway의 Route 등 각종 config들을 설정한다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class })
@AutoConfigureAfter({ GatewayReactiveLoadBalancerClientAutoConfiguration.class,
GatewayClassPathWarningAutoConfiguration.class })
@ConditionalOnClass(DispatcherHandler.class)
public class GatewayAutoConfiguration {
...
@Bean
@Primary
@ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
return new **CachingRouteLocator**(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}
...
}
cachedCompositeRouteLocator 설정한 roteLocator(java, yaml)들이 담기게 된다.

결론
개인적으로는 yaml으로 설정하기보다는 Java Config로 설정하는 게 좋다고 봅니다.
Yaml 설정하면 간단하게 설정 할 수 있는 반면에, IDE의 도움을 받기 힘들고, 오타, yaml 라인이 다른 경우나 yaml 문법 에러 발생 등 정말 누구나 할 수 있는 실수인데, 만약 인지를 못하고 Product로 배포가 되면 장애가 발생합니다.
또한 게이트웨이다보 니 게이트웨이 뒤로 흐르는 모든 application들이 흐를 수 없게 되어 치명적인 장애가 발생할 수 있으니 Production 환경에서는 Java Config로 설정을 하는 걸 추천드립니다.
후기
요즘 내부 구현을 많이 까 보면서, 내가 필요한 부분들을 따로 빼서 커스텀해서 쓰는 일이 몇 번 하다보니, 지금도 마찬가지지만 예전에는 진짜 막 썼었구나라고 다시금 많이 느끼고 그러면서 요즘 스프링과 1cm 정도 거리가 가까워졌다는 걸 느끼면서 물론 그러면서 머리가 빠진... 개인적으로 성장하고 있는 걸 많이 느끼고 있다.
참고
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
https://blog.jungbin.kim/spring/2021/02/27/spring-cloud-gateway-route-locator.html
https://www.baeldung.com/spring-cloud-gateway#spring-cloud-discoveryclient-support
https://dlsrb6342.github.io/2019/05/14/spring-cloud-gateway-구조/
