이 글은 인프런 강의 "Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)"를 정리한 글입니다. 문제/오류가 있을 시 댓글로 알려주시면 감사드리겠습니다
Spring Cloud Gateway 방식을 사용하면 Zuul과 다르게 비동기 처리가 가능해진다
Spring Cloud Gateway는 다음과 같이 동작한다
Client <-> Spring Cloud Gateway <-> Services
Spring Cloud Gateway 내부는 다음과 같이 동작한다
Gateway Handler Mapping : client로부터 gateway에 어떤 요청이 들어왔는지 파악
-> Predicate : 그 요청의 사전 조건(어떤 이름)으로 요청 됐는지 분기
-> PreFilter(사전 필터, Global -> Custom -> Logging) 로 요청 정보 구성
-> First Service, Second Service 등 서블릿으로 요청(Proxied Service)
->PostFilter(사후 필터, Logging -> Custom -> Global) 로 반환 정보 구성
-> 원래의 client에 전달 (Gateway Client)
Spring Cloud Gateway 프로젝트 + Filter 설정(application.yml에 설정)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>apigateway-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>apigateway-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri : http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, first-requests-header1
- AddResponseHeader=first-response, first-response-header1
- id: second-service
uri : http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-requests-header2
- AddResponseHeader=second-response, second-response-header2
server:
port: 8081 #service 2의 경우 8082로 설정
spring:
application:
name : my-first-service #service 2의 경우 my-second-service로 설정
package com.example.firstservice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@RestController
@RequestMapping("/first-service")
public class FirstServiceController {
//header(first-request)를 받아서 header에 저장
@GetMapping("/message")
public String message(@RequestHeader("first-request") String header){
log.info(header);
return "Hello world in First Service";
}
}
Filter 설정(Java)
위에서는 application.yml 파일을 이용해 filter 설정을 했는데 java 코드를 이용한 설정도 가능하다
Apigateway-service만 수정하면 된다
server:
port: 8000
package com.example.apigatewayservice.config;
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 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 객체에 path 정보 추가
.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();
}
}
이후 실행을 하면 yml로 설정했을 때와 동일한 것을 확인할 수 있었다
CustomFilter, GlobalFilter, LoggingFilter
이전 경우는 제공해주는 필터를 적용한 것이었고, 지금부터는 CustomFilter, GlobalFilter, LoggingFilter 을 이용할 것이다
이를 위해서 이전에 설정한 filter 설정은 모두 지워줘야한다
Filter 순서(보편적, 요청 후 돌아오는 형태이기 때문에 사전필터 순서<-> 사후필터 순서)
PreFilter(사전 필터, Global -> Custom -> Logging -> Proxied Service -> PostFilter(사후 필터, Logging -> Custom -> Global)
package com.example.apigatewayservice.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.http.server.reactive.ServerHttpRequest; //webflux
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j
@Component
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(chain)
//비동기 방식에서 사용 -> Mono (단일값)
return chain.filter(exchange).then(Mono.fromRunnable(() ->
{
log.info("Custom Post filter : response code -> {}", response.getStatusCode());
}));
};
}
public static class Config{
//Put the configuration properties
}
}
Custom Filter이 router 하나하나에 직접 적용해야 했던 것이라면 Global Filter은 전체 router에 적용
Global Filter은 가장 먼저 실행되고 가장 마지막에 종료된다
package com.example.apigatewayservice.filter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j
@Component
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(chain)
//비동기 방식에서 사용 -> Mono (단일값)
return chain.filter(exchange).then(Mono.fromRunnable(() ->
{
if(config.isPostLogger()){
log.info("Global Filter end: response code -> {}", response.getStatusCode());
}
}));
};
}
@Data //getter setter
public static class Config{
//Put the configuration properties
private String baseMessage;
private boolean preLogger; // boolean은 is~ 로 자동 생성
private boolean postLogger;
}
}
package com.example.apigatewayservice.filter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter(){
super(Config.class);
}
@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(chain)
//비동기 방식에서 사용 -> Mono (단일값)
return chain.filter(exchange).then(Mono.fromRunnable(() ->
{
if(config.isPostLogger()){
log.info("Logging Post Filter: response code -> {}", response.getStatusCode());
}
}));
}, Ordered.LOWEST_PRECEDENCE); //filter의 order 지정
//우선 순위를 가장 높게 잡았기 때문에 가장 먼저 실행 (Highest_Precedence)
return filter;
}
@Data //getter setter
public static class Config{
//Put the configuration properties
private String baseMessage;
private boolean preLogger; // boolean은 is~ 로 자동 생성
private boolean postLogger;
}
}
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name : GlobalFilter
args :
baseMessage : Spring Cloud Gateway Global Filter
preLogger : true
postLogger : true
routes:
- id: first-service
uri : http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- CustomFilter
- id: second-service
uri : http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- name: CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there
preLogger: true
postLogger: true
service 2로의 실행을 했으며, 앞서 작성한 controller에 check라는 함수를 추가했고, check로 요청을 보냈다
@Slf4j
@RestController
@RequestMapping("/second-service")
public class SecondServiceController {
@GetMapping("/check")
public String check(){
return "Hi, there This is a message from Second Service";
}
}
Spring Cloud Gateway - Eureka 연동
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url :
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name : GlobalFilter # Component 이름
args :
baseMessage : Spring Cloud Gateway Global Filter
preLogger : true
postLogger : true
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
- CustomFilter
- id: second-service
uri: lb://MY-SECOND-SERVICE
predicates:
- Path=/second-service/**
filters:
- name: CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there
preLogger: true
postLogger: true
ApiGateway-Service와 마찬가지로 eureka-client 설정
server:
port: 0
spring:
application:
name : my-first-service
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}} #random port 때문에 instance 로 구분