FrameWork/Spring Cloud
API Gateway Service
태윤2
2021. 8. 25. 20:56
API Gateway Service
- 서비스로 전달되는 모든 API요청의 관문(Gateway) 역할을 하는 서버
- 시스템의 아키텍처를 내부로 숨기고 외부의 요청에 대한 응답만을 적절한 형태로 응답
- 클라이언트는 서비스를 호출하는 게 아닌 API Gateway에 서로 약속한 형태의 요청만 보내면 됨\
장점
- 인증 및 권한 부여 단일 작업
- 서비스(마이크로 서비스) 검색 통합
- 응답 캐싱
- 정책, 회로 차단 및 Qos 다시 시도
- 속도 제한
- 부하 분산(로드 밸런싱)
- 로깅, 추적, 상관관계
더보기
클라이언트의 요청을 일괄적으로 처리
전체 시스템의 부하를 분산시키는 로드 밸런서의 역할
동일한 요청에 대한 불필요한 반복 작업을 줄일 수 있는 캐싱
시스템상을 오고 가는 요청과 응답에 대한 모니터링
시스템 내부에 아키텍처를 숨길 수 있음
Spring Cloud에서의 MSA 간 통신
- RestTemplate
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/", User.calss, 200);
- Feign Client
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value="/store")
List<Store> getStores();
}
Ribbon: Client sid Load Balancer
비동기화 처리가 잘되지 않아 잘 사용하지 않음
Spring Cloud Ribbon, Zuul 은 Spring Boot 2.4에서 maintenance 상태(유지하고 기능을 추가하지 않는 상태)
- 서비스 이름으로 호출
- Health Check
구성
- First Service
- Second Service
- Netflix Zuul
- Routing
- Gate way
First, Second Service
package com.example.firstservice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/")
public class FirstServiceController {
@GetMapping("/welcome")
public String welcome() {
return "Welcome to the First service.";
}
}
server:
port: 8081
spring:
application:
name: my-first-service
eureka:
client:
register-with-eureka: false
fetch-registry: false
- Second 서비스는 First->Second, 8081 -> 8082로 바꿔서 만들어줌
- 의존성은 lombok, Spring Web, Eureka Discovery Client
ZuulService
Zuul로 구현된 게 많아 Zuul사용법을 익히고 Gateway를 공부하는 게 도움된다는 글이 많아 Spring boot 변경을 통해 Zuul 사용법을 먼저 익혀보기로 함!
현재(2021-08-25) 기준 IntelliJ나 SpringInitializr에서 2.4.x 이후 버전만 빌드되기 때문에 직접 build.gradle을 수정함
plugins {
id 'org.springframework.boot' version '2.3.10.RELEASE' ## 수정 1번
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "Hoxton.SR10") ## 수정 2번 (2020.0.0 이하 버전을 사용)
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.3.10.RELEASE' # 수정 3번
implementation 'org.springframework.cloud:spring-cloud-starter-zuul:1.4.7.RELEASE'# 수정 4번
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
- 의존성은 lombok, Spring Web, Eureka Discovery Client
@EnableZuulProxy 추가
package com.example.zuulservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.core.SpringVersion;
@SpringBootApplication
@EnableZuulProxy
public class ZuulServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServiceApplication.class, args);
}
}
application.yml 설정
server:
port: 8000
spring:
application:
name: my-zuul-service
# 라우팅 등록
zuul:
routes:
first-service:
path: /first-service/** # 8000/first-service 로 요청되면 8081 포트로
url: http://localhost:8081
second-service:
path: /second-service/** # 8000/second-service 로 요청되면 8082 포트로
url: http://localhost:8082
ZuulFilter
- 사전, 사후 처리를 해주는 역할(사전 필터, 사후 필터)
package com.example.zuulservice.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Slf4j // lombok
@Component // Component 등록 (용도가 정확하지않을 때 사용 일반적인 Bean 으로 사용하겠다)
public class ZuulLoggingFilter extends ZuulFilter {
@Override
public Object run() throws ZuulException {
// 로거 목적에 따라 info,debug,error,warning 등등 쓰면됨
log.info("************************** printing logs: ");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("************************** " + request.getRequestURI());
return null;
}
//필터의 타입
@Override
public String filterType() {
return "pre";
}
// 여러개가 있을때 순서
@Override
public int filterOrder() {
return 1;
}
// 필터를 쓰겠다 쓰지않겠다
@Override
public boolean shouldFilter() {
return true;
}
}
Spring Cloud Gateway 사용
- 의존성은 Spring Boot DevTools, Eureka Discovery Client, Gateway
- 비동기 처리가 가능(Zuul도 비동기 처리가 추가되었지만 호환성 문제 때문에 Gateway를 현재는 사용함)
application.yml 설정
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-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/**
- 톰캣이 아닌 비동기 Netty 서버 기동
- gateway는 아래 uri 와같은 방식으로 요청을 하게 됨
- zuul은 8000/first-service 로 요청하면 8081 포트로 변경돼서 요청하게 됨
- first, second service contoller 변경
Spring Cloud Gateway - Filter
- Gateway가 요청정보를 받음
- 요청 정보의 사전 조건 분기해주는 Predicate 영역이 있음
- Pre Filter와 Post Filter로 처리함
- Pre Filter의 사전작업을 하고 서비스에 요청 및 응답 이후 Post Filter의 사후 작업을 하고 Client에 응답해줌
Filter using Java Code - FilterConfig.java
- application.yml에 등록한 정보를 java 코드로 구현
- RequestHeader를 추가해서 요청을 보낼 수 있음
- ResponseHeader를 추가해서 응답을 보낼 수 있음
FilterConfig - GatewayService
package com.example.gatewayservice.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(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();
}
}
- first, second controller
package com.example.firstservice;
import lombok.extern.slf4j.Slf4j;
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;
// http://localhost:8081/welcome
// http://localhost:8081/first-service/welcome
@Slf4j
@RestController
@RequestMapping("/first-service/")
public class FirstServiceController {
@GetMapping("/welcome")
public String welcome() {
return "Welcome to the First service.";
}
@GetMapping("/message")
public String message(@RequestHeader("first-request") String header) {
log.info(">>>>>>>>>>>>>>>>>>>>"+header);
return "Hello World in First Service.";
}
}
package com.example.secondservice;
import lombok.extern.slf4j.Slf4j;
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;
// http://localhost:8082/welcome
// http://localhost:8082/second-service/welcome
@Slf4j
@RestController
@RequestMapping("/second-service/")
public class SecondServiceController {
@GetMapping("/welcome")
public String welcome() {
return "Welcome to the Second service.";
}
@GetMapping("/message")
public String message(@RequestHeader("second-request") String header) {
log.info(">>>>>>>>>>>>>>>>>>>>"+header);
return "Hello World in First Service.";
}
}
헤더 확인
application.yml에 Filter 추가하기
Spring Cloud Gateway - CustomFilter
- AbstractGatewayFilterFactory<CustomFilter.Config> 상속받기
- apply 오버라이딩하기
CustomFilter
package com.example.gatewayservice.filter;
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;
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(Config.class);
}
public static class Config {
// Put the configuration properties
}
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom PRE filter: request id -> {}{}", request.getId(), request.getId());
// Custom Post Filter
return chain.filter(exchange)
// Mono-> Spring5 에서 추가된 비동기방식에서 단일값 전달할 때 사용
.then(Mono.fromRunnable(() -> log.info("Custom POST filter: response code -> {}", response.getStatusCode())));
});
}
}
application.yml
first, second service Controller 추가
result
Spring Cloud Gateway - Global Filter
GlobalFilter
package com.example.gatewayservice.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;
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
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)
// Mono-> Spring5 에서 추가된 비동기방식에서 단일값 전달할 때 사용
.then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("Global filter End: response code -> {}", response.getStatusCode());
};
}));
});
}
@Data
public static class Config {
// Put the configuration properties
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
application.yml
result
Spring Cloud Gateway - Custom Filter (Logging)
LoggingFilter
package com.example.gatewayservice.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;
@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return 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);
}
@Data
public static class Config {
// Put the configuration properties
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
application.yml
- second filter에만 추가
result
- first-service
- second-service
- Global start -> Custom start -> Logging Start, End -> Custom end -> Global end 여야함
우선순위를 조절해 순서를 커스텀 할 수 있다
- reference