[MSA] API Gateway - 머리부터 발끝까지(2)
간단한 API Gateway 구축하기
환경
JAVA : 8
SpringBoot : 2.2.5 (글 작성 시점 Spring Cloud Gateway Generally Available 버전)
Gradle : 6.7.1
IDE : Intellij
- 여러 프로젝트를 관리해야 하기 때문에 멀티모듈로 시도해보았다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.2.5.RELEASE' //spring cloud gateway에서 사용하는 ga(Generally Available)
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
//전체에 적용하고 싶다면 allprojects를 사용하면 된다.
//내부 모듈에만 적용하고 싶다면 subprojects
//allprojects와 subprojects에는 plugins를 쓸 수 없어 apply plugin을 사용해야 한다.
allprojects {
apply plugin : 'java'
apply plugin : 'org.springframework.boot'
apply plugin : 'io.spring.dependency-management'
group = 'com.msa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
//spring cloud gateway를 위한 라이브러리
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-gateway', version: '2.2.5.RELEASE'
// netflix에서 Circuit Breaker Pattern을 구현한 라이브러리리
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-hystrix', version: '2.2.5.RELEASE'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
}
test {
useJUnitPlatform()
}
application.yml
Spring Cloud Gateway는 크게 3 부분으로 구성된다.
Route : 특정 Request에 대한 목적 URI / predicates와 하나 이상의 filter로 구성된다.
Predicate : 일치하는 조건을 나타냄 / path 또는 request header에 특정 값을 포함될 수 있다.
filter : Spring WebFilter의 인스턴스 / before, after 작업으로 요청이나 응답에 대한 작업을 수행할 수 있다.
아래 설정은 Gateway에 대한 설정이다.
server:
port: 8080
---
spring:
cloud:
gateway:
default-filters: # 공통 필터
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway GlobalFilter
preLogger: true
postLogger: true
routes: #라우팅 설정/ client1, client2 총 두 개의 마이크로 서비스 라우팅
- id: client1 #조건부에 따라서 http://localhost:8080/client1/ 이 호출된다면 http://localhost:8081/client1/ 서비스가 호출된다.
uri: http://localhost:8081/
predicates:
- Path=/client1/**
filters: # 각 서비스 호출 전 호출되는 필터
- name: Client1Filter
args:
baseMessage: Spring Cloud Gateway Client1Filter
preLogger: true
postLogger: true
- id: client2
uri: http://localhost:8082/
predicates:
- Path=/client2/**
filters:
- name: Client2Filter
args:
baseMessage: Spring Cloud Gateway Client2Filter
preLogger: true
postLogger: true
#게이트웨이 프로세스 추적
logging:
level:
org.springframework.cloud.gateway: DEBUG
reactor.netty.http.client: DEBUG
API가 gateway를 타게된다면, 공통 filter를 거치고 라우팅 설정에 따라서 URI와 적합한 predicates에 맞게 라우팅되게된다.
그 후 각각에 해당하는 개별 filter 설정을 거치게된다.
CommonFilter
Filter 어떠한 방식으로 요청/응답에 대한 작업을 할지 구현을 해주어야한다.
@Slf4j
@Component
public class CommonFilter extends AbstractGatewayFilterFactory<CommonFilter.ConfigField> {
public CommonFilter(){
super(ConfigField.class);
}
//application.yml에 filter args 인자값에 대한 생성자를 생성해줘야한다.
@Getter @Setter
public static class ConfigField{
private String baseMessage; //로그 항목에 포함될 사용자 지정 메세지
private boolean preLogger; //요청을 전달하기 전 필터 기록여부를 나타내는 플래그
private boolean postLogger; //프록시 된 서비스에서 응답을 받은 후 필터 기록여부를 나타내는 플래그
}
@Override
public GatewayFilter apply(ConfigField configField) {
...
}
}
AbstractGatewayFilterFactory
- Gateway를 구현하기 위해서, GatewayFilterFactory를 구현해야하는데, 이때 사용하는 추상 클래스다.좀 더 세분화 된 사용자 지정 filter 작업을 할 수 있다.
AbstractGatewayFilterFactory의 apply 메소드를 오버라이딩해 어떠한 내용을 적용시킬지 보면,
@Override
public GatewayFilter apply(ConfigField configField) {
return ((exchange, chain) -> {
log.info("CommonFilter :"+ configField.toString());
log.info("CommonFilter Message : "+ configField.getBaseMessage());
if (configField.isPreLogger()){
log.info("CommonFilter START : " + exchange.getRequest());
}
return chain.filter(exchange).then(Mono.fromRunnable(() ->{
if (configField.isPostLogger()){
log.info("CommonFilter END : " + exchange.getResponse());
}
}));
});
}
return문의 exchange
가 실제 요청/응답에 대한 값을 담고 있어서 이를 활용해 구현을 하게된다.
요청에 대한 작업 : (exchange, chain) -> {}
응답에 대한 작업 : Mono.fromRunnable(() -> {})
CommonFilter, Client1Filter, Client2Filter 모두 동일한 방식으로 구현했다.
BeforeFilter
이전 글에서 filter 작업에 우선 순위를 부여할 수 있고, batch처럼 before, after 작업을 할 수 있다고 공부했다.
어떻게?
@Slf4j
@Component
public class BeforeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Global Before Filter executed");
return chain.filter(exchange);
}
}
GlobalFilter
를 상속받아 Mono<Void> filter
메소드를 구현하게되면 사전 작업을 수행.
PostFilter
@Slf4j
@Configuration
public class PostFilter {
@Bean
public GlobalFilter postGlobalFilter() {
return (exchange, chain) -> {
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
log.info("Global Post Filter executed");
}));
};
}
}
Mono.fromRunnable()
을 사용해 after 작업을 수행할 수 있다.
어라 위에서 했던거랑 비슷한데?
그렇다. 2가지 방식을 묶어서 CommonFilter에 작성한 방식처럼 사용할 수 있다.
우선순위
filter 우선순위는 생각보다 간단하다.
Ordered
라는 인터페이스를 상속받아 getOrder
함수를 구현해주면 된다. (숫자가 작을수록 우선순위가 높다.)
또는 클래스 상단에 @Order(1) 과 같이 어노테이션으로도 제어가 가능하다.
BeforeOrderedFilter
@Slf4j
@Component
public class BeforeOrderedFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Order number -1 BeforeOrderedFilter "); //before 로직
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
log.info("BeforeOrderedFilter END "); //after 로직
}));
}
//우선순위
@Override
public int getOrder() {
return -1;
}
}
위에서 작성한 BeforeFilter 클래스가 그 다음 필터 역할로 올 수 있도록 Ordered를 추가해보자
BeforeFilter
@Slf4j
@Component
public class BeforeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Global Before Filter executed");
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
log.info("BeforeFilter END "); //after 로직
}));
}
@Override
public int getOrder() {
return 0;
}
}
이제 어플리케이션을 실행시킨 후 호출을 하게 되면 어떤 순서로 동작할까?
실제로 동작 시켜보기 전에 한 번 예측해보자.
localhost:8080/client1/ping 을 호출해보도록 하겠다.
gateway에 대한 로그를 설정해두었기 때문에 보다 자세하게 확인이 가능하다.
client1에 매칭이 되었고, 요청정보에 담긴 Exchange 정보와 route 될 정보 등등에 대한 정보를 확인할 수 있다.
BeforeOrderedFilter - BeforeFilter - CommonFilter - ClientFilter
우선순위를 가진 필터 - 공통 필터 - 개별 필터의 순서대로 적용이 잘 된것을 확인할 수 있다.
마지막 해당 Controller는 아직 구현을 하지 않아서 에러가 발생하였다.
다음에는 Client1을 구현해보자.
참고
https://spring.io/projects/spring-cloud-gateway
https://www.baeldung.com/spring-cloud-custom-gateway-filters
https://medium.com/@niral22/spring-cloud-gateway-tutorial-5311ddd59816