Search

API Gateway

생성일
2024/02/18 07:08
태그
MSA 실습
상태
완료

API Gateway

Discovery Service에 여러 서비스를 등록하고, 이를 통해 해당 서비스의 주소를 쉽게 알아낼 수 있도록 만들었다. 여기에 클라이언트에서 요청을 단일화 할 수 있도록, Gateway를 추가하여 Gateway의 기능과 필터링, 로드 밸런싱 기능을 추가하자.

API Gateway

API Gateway 서비스는 사용자가 설정한 라우팅 설정에 따라, 클라이언트 요청 시 각각의 엔드포인트에 요청을 전달해주고 응답을 받아 다시 클라이언트로 돌려주는 프록시 역할을 수행한다. 시스템 내부 구조를 숨기고 외부의 요청을 적절한 형태로 가공해서 응답할 수 있다는 장점이 있다.
API Gateway 없이 MSA를 구축하면, 이와 같이 클라이언트에서 각 마이크로 서비스의 주소를 전부 알고 있어야한다. 거기에 더해 새로운 마이크로 서비스가 추가되거나 기존 마이크로 서비스의 주소가 변경된다면, 해당 마이크로 서비스의 주소를 클라이언트에도 추가하거나 변경 해주어야 한다. 이렇게 되면 마이크로 서비스 하나만 수정하여 배포하더라도, 클라이언트도 수정하여 재배포해야하는 문제가 생긴다.
이러한 문제를 해결하기 위해 서버단 중간에 Gateway 역할을 해줄 수 있는 일종의 진입로를 두고, 각 마이크로 서비스로 요청되는 모든 정보들에 대해 일괄적으로 처리할 수 있게 만들어 주는 것을 API Gateway라고 한다.
여기에 더해 인증 및 권한 부여, 서비스 통합 검색, 응답 캐싱, 정책, 속도 제한, 부하 분산, 로깅, 추적, 차단 등 요청 흐름 제어를 API Gateway에서 수행할 수 있다.

Spring Cloud의 MSA 간 통신

Spring Cloud에서 MSA 간 통신을 구현하는 방법은 RestTemplate와 Feign Client 두 가지가 있다.
RestTemplate
REST API를 사용하는 RestTemplate 방식은 전통적으로 웹 서비스 애플리케이션에서 다른 애플리케이션을 호출할 때 사용되는 API 방식이다. 파라미터를 전달하여 GET이나 POST 메서드 형식으로 요청을 보내면, 상대 애플리케이션에서는 요청을 받아 이에 대한 처리를 수행하는 방식이다.
Feign Client
스프링 클라우드에는 Spring Cloud Feign Client라는 API를 이용해서 호출할 수 있다.
이와 같이 인터페이스를 하나 생성하여, 직접적인 마이크로 서비스의 주소나 포트 번호 없이 마이크로 서비스 이름을 통해 호출할 수 있는 방식이다.

Feign Client

Netflix Ribbon
이러한 Feign Client 기술은 여러 프로젝트를 통해 사용할 수 있는데, 과거에는 Netflix Ribbon을 사용하였다.
Ribbon은 이러한 API Gateway 기능에 Load Balancer 기능과 Health check 기능을 더한 Client Side Load Banlancer로, 클라이언트 단에 추가되어 마이크로 서비스의 주소 없이도 이름만 가지고 접근할 수 있도록 해준다. Ribbon은 비동기에 대한 처리가 약해 최근에는 잘 사용되지 않는다.
Netflix Zuul
API Gateway를 구현하는 또 다른 방법은 Netflix Zuul이라는 별도의 서비스를 두고 Gateway 역할을 수행하도록 구성하는 것이다.
Netflix Zuul 역시 라우팅이나 API Gateway 기능을 모두 수행할 수 있지만, Ribbon과 마찬가지로 더이상 사용할 수 없다(Deprecated).
Spring Cloud Loadbalancer / Spring Cloud Gateway
Spring Cloud Loadbalancer, Spring Cloud Gateway는 각각 Ribbon과 Netflix Zuul가 Deprecated 됨으로써, 그에 대한 대안으로 Spring에서 제시하는 기능이다. 이를 통해 Loadbanlancer 역할과 Gateway 역할을 하는 서비스를 구축할 수 있다.
기존의 방식과의 차이점은 비동기 처리가 가능하다는 점이다. 비동기 처리가 약한 Ribbon이나 Servlet 작업에서 동기 방식을 사용하는 Zuul에서는, 최신 트렌드에 맞는 함수형 프로그래밍이나 비동기 방식을 지원하지 못했다. Zuul이 버전 2에 들어서 비동기를 지원하긴 하지만, 스프링의 나머지 라이브러리와 호환성 문제가 있어 스프링에서는 자체적으로 Gateway라는 서비스를 만들어 Zuul 서비스를 대체하였다.

Spring Cloud Gateway로 API Gateway 구축하기

서비스 생성

API Gateway를 구축하기 전에 Gateway에 연결하여 라우팅 받을 서비스를 2개 만들어보자.
// first-service @RestController @RequestMapping("/first-service") public class FirstServiceController { @GetMapping("/welcome") public String welcome() { return "welcome to the first service!!"; } } // second-service @RestController @RequestMapping("/second-service") public class SecondServiceController { @GetMapping("/welcome") public String welcome() { return "welcome to the second service!!"; } }
Java
복사
이와 같이 간단하게 문자열을 출력하도록 API를 만들어두고,
server: port: 8081 spring: application: name: first-service
YAML
복사
server: port: 8082 spring: application: name: second-service
YAML
복사
이와 같이 포트 번호와 서비스 이름을 지정해준다.
이와 같이 설정한 포트번호로 접속하면 입력해둔 문자열이 잘 출력된다.

API Gateway 생성

<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
XML
복사
이와 같이 spring cloud gateway, netflix eureka, lombok 라이브러리 의존성을 추가해주고,
server: port: 8000 eureka: client: fetch-registry: false register-with-eureka: false service-url: defaultZone: http://localhost:8761/eureka spring: application: name: api-gateway-service cloud: gateway: routes: - id: first-service uri: http://localhost:8081/ predicates: - Path=/first-service/** - id: second-service uri: http://localhost:8082/ predicates: - Path=/second-service/**
YAML
복사
이처럼 사용할 포트, 이후 사용할 유레카 서비스, 서비스 이름을 지정해준다. 그리고 gateway로서 기능할 설정을 추가해야하는데, cloud.gateway.routes에 아까 만들었던 라우팅할 서비스를 추가해준다. Gateway 서버의 포트로 Path 형태의 API 요청을 받으면, uri 주소로 라우팅하여 uri + Path 형태로 요청을 전달하고 응답을 받아 반환한다.

Gateway 동작 확인하기

아까 생성해둔 서비스 2개와 Gateway service를 모두 실행시킨 후, 실제 Gateway가 라우팅을 잘 수행하는지 확인해보자.
Gateway 포트인 8000번 포트로 접속했지만, 생성해둔 서비스로 라우팅되어 미리 입력해둔 문자열이 잘 출력되는 것을 확인할 수 있다.
second-service도 마찬가지로 8000번 포트로 접속했지만, 해당 서비스가 라우팅을 받아 잘 동작된다.

트러블슈팅

Tomcat 서버
여기서 주의할 점은 강의에서는 Spring Initializer를 통해 프로젝트를 생성하는데, 이는 과거 시점에 녹화된 영상이라서 동일하게 수행해도 다른 라이브러리 의존성 파일이 추가된다.
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway-mvc</artifactId> </dependency> ... </dependencies>
XML
복사
이처럼 spring-cloud-starter-gateway-mvc 라이브러리가 추가된다.
이 라이브러리를 사용하려면, 위(강의의 yml)와 동일하게 설정 파일을 작성하면 안되고 아래와 같이 사용해야한다.
spring: cloud: gateway: mvc: routes: - id: first-service uri: http://localhost:8081/ predicates: - Path=/first-service/** - id: second-service uri: http://localhost:8082/ predicates: - Path=/second-service/**
YAML
복사
강의에서는 내장 서버가 Netty가 동작을 하는데, 이 방법을 적용하면 내장 서버가 Tomcat으로 실행된다. 강의에서는 Netty는 비동기 처리가 되고 Tomcat은 안되는 것처럼 이야기를 하였으나, 검색해본 결과 톰캣도 NIO(New Input Output) 커넥터를 통해 비동기 처리가 가능한 것처럼 보였다.
그래도 일단은 강의를 따라하며 toy 프로젝트로 MSA를 구축해보고 있으니까, 강의와 동일하기 사용하기 위하여 Netty로 실행시키기 위해서는 아래와 같이 수정해야한다.
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> ... </dependencies>
XML
복사
M1 이슈
또 다른 트러블 슈팅으로 MacOs M1 노트북 이슈가 있었다. 내 MacOs M1 노트북으로 Gateway를 실행시킨 후 라우팅을 시도하면, 정상적으로 동작은 하지만 로그에 아래와 같은 에러가 발생했다.
검색해본 결과 이는 netty 서버와 macOS의 호환성 문제라 하여,
<dependency> <groupId>io.netty</groupId> <artifactId>netty-resolver-dns-native-macos</artifactId> <classifier>osx-aarch_64</classifier> </dependency>
XML
복사
위처럼 netty-resolver-dns-native-macos 라이브러리 의존성을 추가해주면 해결이 된다.

필터링 및 로드 밸런싱

Spring Cloud Gateway의 동작을 자세히 살펴보면,
Gateway Hanndler Mapping으로 클라이언트에서 어떤 요청이 들어왔는지 확인하고, Predicate로 등록된 사전 조건을 확인하여 어떤 서비스로 가야할지 분기해준다. 그 이후 사전 필터를 거쳐 해당 서비스에 요청을 전달하고, 응답을 받을 때 사후 필터를 거쳐 클라이언트로 다시 반환한다.

Gateway에 필터 추가하기

Java 코드로 추가하기
spring: application: name: api-gateway-service # cloud: # gateway: # routes: # - id: first-service # uri: http://localhost:8081/ # predicates: # - Path=/first-service/** # - id: second-service # uri: http://localhost:8082/ # predicates: # - Path=/second-service/**
YAML
복사
java 코드로 Config 파일을 추가하여 Gateway 설정을 해주기 위해, 이전에 작성했던 yml 파일에 gateway 관련 설정을 주석처리해준다.
@Configuration public class FilterConfig { @Bean public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("/first-service/**") .filters(f -> f.addRequestHeader("first-request", "first-request-header") .addResponseHeader("first-response", "first-response-header")) .uri("http://localhost:8081")) .route(r -> r.path("/second-service/**") .filters(f -> f.addRequestHeader("second-request", "second-request-header") .addResponseHeader("second-response", "second-response-header")) .uri("http://localhost:8082")) .build(); } }
Java
복사
이후 Filter 설정을 해줄 Configuration에 라우팅을 수행할 빈을 등록해준다. 라우팅은 아까 yml 파일에서 주석처리한 라우트 설정과 이번에 새로 추가할 필터 설정을 추가한다.
필터는 클라이언트에서 받은 요청 헤더에 추가할 RequsetHeader를 설정해주고, 서비스에서 응답을 받아 클라이언트에 전달할 때 응답 헤더에 추가할 ResponseHeader를 설정해주었다.
// first-service @GetMapping("/message") public String message(@RequestHeader("first-request") String header) { log.info("header = {}", header); return "Hello world in second service"; } // second-service @GetMapping("/message") public String message(@RequestHeader("second-request") String header) { log.info("header = {}", header); return "Hello world in second service"; }
Java
복사
이와 같이 헤더를 열어 로그로 출력하는 별도의 요청을 만들고, 우리가 추가한 헤더가 잘 담겨서 주고 받는지를 확인해보자.
이처럼 요청 헤더를 잘 받아오는 것을 볼 수 있고,
응답 헤더 또한 잘 들어있는 것을 확인할 수 있다.
환경 설정(yml 파일)으로 추가하기
yml 파일을 통해 필터를 추가하는 방법은 이전에 했던 방식에서 코드만 약간 수정해주면 된다.
spring: cloud: gateway: routes: - id: first-service uri: http://localhost:8081/ predicates: - Path=/first-service/** filters: - AddRequestHeader=first-request, first-requests-header2 - AddResponseHeader=first-request, first-response-header2 - id: second-service uri: http://localhost:8082/ predicates: - Path=/second-service/** filters: - AddRequestHeader=second-request, second-requests-header2 - AddResponseHeader=second-request, second-response-header2
YAML
복사
이와 같이 환경 설정에 filter에 대한 정보를 추가해주면,
이전과 마찬가지로 요청 시에 필터에우리가 설정한 값이 header에 잘 들어있는 것을 볼 수 있다.
응답에도 헤더가 잘 들어있는 것을 볼 수 있다.

Custom Filter 적용하기

@Component @Slf4j public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> { public CustomFilter() { super(Config.class); } @Override public GatewayFilter apply(Config config) { // Custom Pre-Filter return ((exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); log.info("Custom PRE filter: request id = {}", request.getId()); //Custom Post filter return chain.filter(exchange).then(Mono.fromRunnable(() -> { log.info("Custom POST filter : response code = {}", response.getStatusCode()); })); }); } public static class Config { // Put the configuration properties } }
Java
복사
이와 같이 AbstractGatewayFilterFactory를 확장하는 CustomFilter를 하나 구현해두고, exchange와 chain을 매개변수로 받는 람다식으로 pre-filter와 post-filter를 구현하는 방식이다. apply는 람다식 형태의 익명 함수를 함수형 인터페이스로 반환하여, 반환한 람다식을 빈으로 등록하여 필터로 동작하게 만드는 메서드이다. 람다식 안에 pre-filter에서 수행할 동작과, 람다식의 반환값에 chain을 통해 post-filter에서 수행할 동작을 넣어 각각 사전 필터와 사후 필터로 동작할 수 있게 된다.
필터에서 요청 데이터를 출력해보기 위해, exchange로 부터 Server의 Request와 Response를 얻어야 한다. 각각 org.springframework.http.server.reactive.ServerHttpRequest / ServerHttpResponse인데, reactive가 포함되는 ServerHttp를 임포트해야 RxJava라고 해서 Webflux를 지원하는 Spring 5의 기능을 사용할 수 있다. Post filter에서 호출하는 Mono 역시 Webflux라는 Spring 5 기능으로, 비동기 방식으로 수행할 람다식(익명함수)을 단일값으로 전달할 때 사용된다.
spring: cloud: gateway: routes: - id: first-service uri: http://localhost:8081/ predicates: - Path=/first-service/** filters: # - AddRequestHeader=first-request, first-requests-header2 # - AddResponseHeader=first-request, first-response-header2 - CustomFilter - id: second-service uri: http://localhost:8082/ predicates: - Path=/second-service/** filters: # - AddRequestHeader=second-request, second-requests-header2 # - AddResponseHeader=second-request, second-response-header2 - CustomFilter
YAML
복사
이후 yml 파일에 우리가 만든 CustomFilter 등록해주면 각 서비스에 요청을 전달할 때 사전, 사후필터가 동작하게 된다.
@GetMapping("/check") public String check() { return "Hi, there. This is a message from First Service"; }
Java
복사
동작을 확인하기 위해 이와 같이 간단한 메세지를 출력하는 API를 만들어두고 요청을 보내면,
이처럼 요청 시마다 Gateway에 로그가 잘 찍히는 것을 확인할 수 있다.

Global Filter 적용하기

Global Filter는 그 과정과 원리는 Custom Filter와 동일하지만, Custom Filter는 필요한 라우팅마다 라우트 설정을 추가해주어야 하지만 Global Filter는 모든 라우팅에 일괄적으로 적용되는 차이가 있다. 또한 글로벌 필터로 등록하게 되면, 요청 시 모든 사전 필터 중에 가장 먼저 실행되고 응답 시 모든 사후 필터 중 가장 나중에 실행된다.
@Component @Slf4j public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> { public GlobalFilter() { super(Config.class); } @Override public GatewayFilter apply(Config config) { // Custom pre-Filter return ((exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); log.info("Global Filter baseMessage: {}", config.getBaseMessage()); if (config.isPreLogger()) { log.info("Global Filter start: request id = {}", request.getId()); } //Custom Post filter return chain.filter(exchange).then(Mono.fromRunnable(() -> { if (config.isPostLogger()) { log.info("Global Filter end: response code = {}", response.getStatusCode()); } })); }); } @Data public static class Config { private String baseMessage; private boolean preLogger; private boolean postLogger; } }
Java
복사
Global Filter를 위와 같이 추가해준다. Global Filter는 yml과 같은 환경 설정에서 미리 파라미터(baseMessage, preLogger, postLogger)를 정의해두고, 해당 값에 따라 필터의 동작 여부를 결정하도록 작성되었다.
spring: cloud: gateway: default-filters: - name: GlobalFilter args: baseMessage: Spring Cloud Gateway Global Filter preLogger: true postLogger: true
YAML
복사
yml 파일에는 이전에 CustomFilter처럼 각 라우트 별로 필터를 등록하는 것이 아니라, 이와 같이 spring.cloud.gateway 하위에 default-filters로 등록하여 모든 라우팅 시에 동작하도록 구성한다. 추가적으로 아까의 Config 설정에 전달할 파라미터들(baseMessage, preLogger, postLogger) 값을 정의하여 환경 설정만으로 전략을 변경할 수 있다.
서버를 재시작하고 동작을 확인해보면,
이처럼 Gateway에 요청이 라우팅 될 때 가장 먼저 GlobalFilter의 사전 필터가 동작하고 가장 마지막에 GlobalFilter의 사후 필터가 동작하는 것을 볼 수 있다.

Logging Filter 추가하기

이전의 CustomFilter와 동일하게 Second-service에만 로그를 출력하는 LoggingFilter를 적용하면서,
이와 같은 순서를 가지는 필터 체인을 가지도록 구성할 예정이다.
routes: ... - id: second-service uri: http://localhost:8082/ predicates: - Path=/second-service/** filters: # - AddRequestHeader=second-request, second-requests-header2 # - AddResponseHeader=second-request, second-response-header2 - CustomFilter - name: LoggingFilter args: baseMessage: Hi, there. preLogger: true
YAML
복사
이처럼 GlobalFilter처럼 파라미터를 정의하면서 Second-service에만 동작하도록 yml 파일을 추가해준다.
LoggingFilter 클래스를 정의하기에 앞서 이번에는 CustomFilter와의 차이는 apply 메서드에서 GatewayFilter 함수 인터페이스로 반환하는 람다식 대신, GatewayFilter의 구현체를 통해 사용하는 방법을 알아보자. 우리는 apply 메서드는 GatewayFilter 인터페이스를 반환하는데, 이 GatewayFilter 인터페이스의 구현체는 여러가지가 있지만 그 중 OrderedGatewayFilter를 통해 구현할 것이다.
GatewayFilter filter = new OrderedGatewayFilter(...); // OrderedGatewayFilter public class OrderedGatewayFilter implements GatewayFilter, Ordered { private final GatewayFilter delegate; private final int order; public OrderedGatewayFilter(GatewayFilter delegate, int order) { this.delegate = delegate; this.order = order; } public GatewayFilter getDelegate() { return this.delegate; } public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return this.delegate.filter(exchange, chain); } public int getOrder() { return this.order; } public String toString() { return "[" + this.delegate + ", order = " + this.order + "]"; } }
Java
복사
이 OrderdGatewayFilter는 위임하여 필터를 수행할 GatewayFilter와 필터의 순서를 결정하는 order를 필드로 가지고 있다.
GatewayFilter 인터페이스를 살펴보면, 필터 동작을 수행하는 filter 메서드는 ServerWebExchange 클래스를 받는 exchange와 GatewayFilterChain 클래스를 받는 chain을 파라미터로 받는다.
ServerWebExchange은 Spring의 Webflux 2.0 기능으로, 기존의 ServletRequest와 ServletResponse는 Webflux에서는 사용되지 않고 ServerRequest와 ServerResponse를 사용하는데 이러한 ServerRequest와 ServerResponse를 사용할 수 있도록 해주는 것이 WebExchange이다.
GatewayFilterChaing은 pre 필터나 post 필터, pre chain, post chain 등 다양한 필터를 연결시켜 주는 역할을 한다.
@Override public GatewayFilter apply(Config config) { GatewayFilter filter = new OrderedGatewayFilter(((exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); log.info("Logging Filter baseMessage: {}", config.getBaseMessage()); if (config.isPreLogger()) { log.info("Logging Pre Filter: request id = {}", request.getId()); } //Custom Post filter return chain.filter(exchange).then(Mono.fromRunnable(() -> { if (config.isPostLogger()) { log.info("Logging Post Filter: response code = {}", response.getStatusCode()); } })); }), Ordered.HIGHEST_PRECEDENCE); return filter; }
Java
복사
때문에 이와 같이 OrderdGatewayFilter의 생성자에 GatewayFilter로 필터로 동작시킬 람다식을 넘겨주면 된다. 추가적으로 Orderd.HIGHEST_PRECEDENCE와 같이 order에 대한 정보도 같이 넣어줄 수 있다.
동작을 확인해보면 다음과 같다.
우리가 초기에 목표했던 바와 다르게, LoggingFilter → GlobalFilter → CustomFilter → Service → CustomFilter → GlobalFilter → LoggingFilter 순으로 동작한다.
이는 우리가 OrderedGatewayFilter의 생성자에 order 값으로 넣어준 값이 Orderd.HIGHEST_PRECEDENCE이어서 그렇다. 앞서 설명했듯 GlobalFilter는 모든 사전 필터 중 가장 먼저 동작하고 모든 사후 필터 중 가장 마지막에 동작하는데, LoggingFilter로 가장 높은 순서를 가지도록 설정해주었으니 이를 무시하고 필터 체인의 가장 앞에 동작하는 것이다.
@Override public GatewayFilter apply(Config config) { GatewayFilter filter = new OrderedGatewayFilter(((exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); log.info("Logging Filter baseMessage: {}", config.getBaseMessage()); if (config.isPreLogger()) { log.info("Logging Pre Filter: request id = {}", request.getId()); } //Custom Post filter return chain.filter(exchange).then(Mono.fromRunnable(() -> { if (config.isPostLogger()) { log.info("Logging Post Filter: response code = {}", response.getStatusCode()); } })); }), Ordered.LOWEST_PRECEDENCE); return filter; }
Java
복사
이와 같이 가장 낮은 순서로 바꾸어서 넣어주면,
우리가 의도했던 순서대로 필터 체인이 동작한 것을 확인할 수 있다.

Load Balancer

이제 구현된 사항들로 위과 같이 Gateway에서 Service Discovery를 거쳐 서비스의 위치를 받고, 그 위치를 통해 서비스에 요청을 전달하는 과정을 해보자.
eureka: client: fetch-registry: true register-with-eureka: true service-url: defaultZone: http://localhost:8761/eureka
YAML
복사
먼지 두 개의 서비스와 Gateway를 Service Discovery에 추가하기 위해, eureka 클라이언트 설정을 켜고 유레카 서버 주소를 설정 파일에 넣어 서비스를 등록하도록 수정한다.
spring: cloud: gateway: routes: - id: first-service uri: lb://FIRST-SERVICE predicates: - Path=/first-service/** filters: - CustomFilter - id: second-service uri: lb://SECOND-SERVICE predicates: - Path=/second-service/** filters: - CustomFilter - name: LoggingFilter args: baseMessage: Hi, there. preLogger: true postLogger: true
YAML
복사
그 후에 Gateway의 yml 파일에서 라우팅할 주소를 Service Discovery에서 받아온 주소로 보내도록 설정한다. 여기서 lb는 API 유레카 네이밍으로 서비스를 찾아 포워딩 및 로드 밸런싱을 하겠다는 명령어이다.
유레카 서버에 서비스와 Gateway가 잘 등록되어 있는 것을 확인할 수 있고,
Gateway의 기능도 잘 수행되는 것을 확인할 수 있다.
여기에 더해 이전에 했었던 동적 포트 지정 방식을 적용하여 같은 서비스를 여러 개 띄워보자.
server: port: 0
YAML
복사
서버의 포트를 0번으로 지정하면 spring에서 남는 포트 중 하나를 동적으로 지정하게 되고,
eureka: client: fetch-registry: true register-with-eureka: true service-url: defaultZone: http://localhost:8761/eureka instance: instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
YAML
복사
Service Discovery에서 동일한 이름으로 보이는 것을 피하기 위해 instanc-id를 지정해준다.
이후 서비스를 여러 개 실행시켜보면,
이와 같이 유레카 서버에 여러 개의 서비스가 등록되어 있는 것을 확인할 수 있다.
이후 서비스에서 어떤 포트에서 실행되는지를 확인하기 위해,
@GetMapping("/check") public String check(HttpServletRequest request) { log.info("Server port = " + request.getServerPort()); return "Hi, there. This is a message from First Service On Port " + request.getServerPort(); }
Java
복사
이와 같이 check API를 호출하면 포트 번호를 출력 및 반환하도록 추가해주자.
그 후 요청을 여러 번 동작을 확인해보면,
포트가 한 번씩 번갈아가며 바뀌어 출력이 된다. 이는 Gateway가 라운드 로빈 방식으로 마이크로 서비스의 부하를 분산하여 호출하여, 서비스가 2개 밖에 없어 매 요청마다 서로 번갈아가며 호출되는 것이다.
추가적으로 현재 포트 번호를 랜덤 포트로 변경했는데, 그럼에도 Service Discovery에 등록된 이름을 통해 주소와 포트 번호를 찾아 요청을 전달하기 때문에 문제 없이 잘 동작하는 것을 확인할 수 있다.
이처럼 Spring Cloud Gateway를 사용하면 Gateway를 통한 라우팅 기능과 로드 밸런싱 기능을 쉽게 사용할 수 있다.

참고