1. 앞서 알고가기
Spring Cloud Gateway 로 API Gateway 구축에 앞서 필요한 개념을 먼저 간단하게 정리해봤다.
1-1. API Gateway 란?
인증 / 모니터링 / 오케스트레이션 등과 같은 기능을 포함한 향상된 Reverse Proxy다. Netflix zuul, Amazon API Gateway, Spring Cloud Gateway 같은 것들이 잘 알려진 Api Gateway 구현체들이다.
1-2. Spring Cloud Gateway (이하 SCG) 란?
Spring Reactvie 생태계에 구현된 API Gateway이다. Gateway Handler Mapping으로 들어오는 요청들을 적절한 대상으로 라우팅하는 간단하고 효과적인 방법을 제공한다.
그리고 Spring Cloud Gateway는 논블로킹(non-blocking), 비동기(Asynchronous) 방식의 Netty Server를 사용한다. 때문에 서블릿 컨테이너나 WAR로 빌드하면 동작하지 않는다.
아래는 SCG의 라우팅이 작동하는 방식에 대한 흐름도다.
SCG는 3가지 핵심 단위로 이루어져있다.
1. Route: 대상 URI, 조건부 집합(Predicates), 각종 필터들로 이루어진 요청을 라우팅할 대상들이라고 생각하자.
2. Predicate: Java 8의 Function Predicate로, 라우팅에 필요한 조건이다. 예로 path = /abc 같은 조건이나, request header의 특정 키-값도 조건으로 사용할 수 있다.
3. Filter: Spring Framework의 WebFilter 인스턴스이다. Filter에서는 요청 전후로 요청 / 응답을 추가/ 수정 할 수 있다.
2. 구현
샘플로 구현할 프로젝트의 구성은 1개의 Gateway와 2개의 MSA로 구성하고, Gateway에서 2개의 서비스를 라우팅할 것이다.
2-1 API Gateway
build.gradle
plugins {
id 'org.springframework.boot' version '2.3.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
allprojects {
group = "kr.taeu"
version = "0.0.1-SNAPSHOT"
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
repositories {
mavenCentral()
}
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencyManagement {
imports {
mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Hoxton.SR8'
}
}
dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
developmentOnly("org.springframework.boot:spring-boot-devtools")
implementation("org.springframework.boot:spring-boot-starter-actuator")
annotationProcessor("org.projectlombok:lombok")
compileOnly("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude group: ("org.junit.vintage"), module: ("junit-vintage-engine")
}
}
}
project (':gateway-server') {
dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-gateway")
}
}
SCG의 라우팅 코드 작성방식은 Java-config 방식과 xml 방식이 있는데, 여기서는 xml 방식으로 빠르게 구현하겠다.
gateway-server의 resources 디렉토리에 application.yml을 생성하고 다음과 같이 입력한다.
application.yml
# Spring Cloud Gateway port
server:
port: 8080
spring:
cloud:
gateway:
# Gateway 공통 필터
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway GlobalFilter
preLogger: true
postLogger: true
# MSA 라우팅 정의
# ID, 목적지(uri), 조건(Predicate), 필터로 구성된다.
routes:
- id: user-service
# 목적지
uri: http://localhost:8081
# 조건부 집합 Header나 Parameter같은 HTTP 요청의 모든 항목 비교
# 아래와 같은 경우 localhost:8080/user 호출하게 되면 localhost:8081/user로 라우팅 된다.
predicates:
- Path=/user/**
# GatewayFilter 인스턴스, Filter에서는 다운스트림 요청 전후에 요청/응답 가능
filters:
- name: UserFilter
args:
baseMessage: Taeu UserFilter
preLogger: true
postLogger: true
- id: shop-service
uri: http://localhost:8082
predicates:
- Path=/shop/**
filters:
- name: ShopFilter
args:
baseMessage: Taeu ShopFilter
preLogger: true
postLogger: true
default-filters: 전역 필터를 설정한다.
routes: SCG의 Route 설정을 담당한다. predicates의 조건 구문을 통해 uri로 라우팅하게 된다, 필터들은 직접 구현하거나 Spring이 제공하는 필터를 사용해도 되는데 여기서는 직접 구현하여 추가해보도록 한다.
위 설정을 토대로 상황을 시뮬레이션 해보면
http://localhost:8080/user 호출시 path=/user/** 조건에 의해 http://localhost:8081/user로 라우팅 될 것이고, 그 과정에서 GlobalFilter를 거쳐 UserFilter를 거치게 될 것이다.
필터의 구현은 GlobalFilter, UserFilter, ShopFilter를 구현할 것인데, 셋의 내용은 동일하므로 여기서는 GlobalFilter만 기술한다.
(참고: https://cloud.spring.io/spring-cloud-gateway/multi/multi__developer_guide.html)
GlobalFilter.java - 전역필터
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class GlobalFilter extends AbstractGatewayFilterFactory<Config> {
public GlobalFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(final Config config) {
return (exchange, chain) -> {
log.info("GlobalFilter baseMessage: {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("GlobalFilter Start: {}", exchange.getRequest());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("GlobalFilter End: {}", exchange.getResponse());
}
}));
};
}
}
AbstractGatewayFilterFactory: GatewayFilter 구현을 위한 추상클래스이다.
exchange: ServerWebExchange 인스턴스로, HTTP 엑세스를 제공한다. (요청 / 응답 에 대한 접근 등) GatewayFilter (exchange, chain) 를 통하여 사용가능하다. 예제에서는 (exchange, chain) 이후로 request에 대한 접근을 하고 chain.filter(exchange)를 통하여 response를 얻은 후의 접근은 .then(Mono.fromRunnable 이후로 한다.)
Config.java - Filter에 설정한 인자값을 위한 값 클래스
package kr.taeu.gateway.filters;
import lombok.Getter;
@Getter
public class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
public Config(String baseMessage,
boolean preLogger,
boolean postLogger) {
this.baseMessage = baseMessage;
this.preLogger = preLogger;
this.postLogger = postLogger;
}
}
위까지 설정을 했으면 gateway가 정상 작동하는지 http://localhost:8080/user를 호출해보자. 아래와 같이 에러가 나면 정상적으로 라우팅은 되는것, 아직 서비스를 구현안했으니 에러가 발생한다.
2-2 Services
gateway server에서 설정한대로, user 서비스는 8081, shop 서비스는 8082로 설정한다.
user-service의 resources/application.yml
server:
port: 8081
shop-service의 resources/application.yml
server:
port: 8082
Controller를 각 서비스마다 만들어준다.
user-service의 controller/UserController.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping
public Mono<String> get(final ServerHttpRequest request,
final ServerHttpResponse response) {
log.info("User MSA Start");
final HttpHeaders httpHeader = request.getHeaders();
httpHeader.forEach((key, values) -> log.info("{}: {}", key, values));
log.info("User MSA End");
return Mono.just("User MSA Response");
}
}
shop-service의 controller/ShopController.java
package kr.taeu.shop.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@Slf4j
@RestController
@RequestMapping("/shop")
public class ShopController {
@GetMapping
public Mono<String> get(final ServerHttpRequest request,
final ServerHttpResponse response) {
log.info("Shop MSA Start");
final HttpHeaders httpHeader = request.getHeaders();
httpHeader.forEach((key, values) -> log.info("{}: {}", key, values));
log.info("Shop MSA End");
return Mono.just("Shop MSA Response");
}
}
3. 결과
gateway와 서비스들을 전부 실행 시켜주고, api를 호출해보자
http://localhost:8080/user
gateway에서의 필터 로그는 다음과 같이 찍힌다
GlobalFilter -> Service Filter 순으로 실행되는 걸 볼 수 있다.
API Gateway는 reverse proxy를 향상시켜 확장한 개념이다. 모든 요청이 gateway를 통하므로, 인증/보안, 모니터링으로의 확장이 용이하다.
Github
https://github.com/lteawoo/spring_cloud_gateway_demo
참고
https://medium.com/@niral22/spring-cloud-gateway-tutorial-5311ddd59816
https://cloud.spring.io/spring-cloud-gateway/multi/multi__developer_guide.html
'Spring' 카테고리의 다른 글
Thymeleaf - 표준 표현식 (0) | 2021.01.27 |
---|---|
SpringBoot Mysql Datasource 세팅 (0) | 2020.11.16 |
Spring Security OAuth2 - Authorization endpoint (0) | 2020.10.07 |
Spring Jpa - Query DSL + Gradle 6 설정 (3) | 2020.09.21 |
Spring Jpa - Paging api 처리하기 (0) | 2020.09.18 |