API Gateway Service
API Gateway๋ ์๋น์ค๋ก ์ ๋ฌ๋๋ ๋ชจ๋ API ์์ฒญ์ ๊ด๋ฌธ ์ญํ ์ ํ๋ค. ๋ด๋ถ ์๋ฒ ์์คํ ์ ์ํคํ ์ฒ๋ฅผ ๋ด๋ถ๋ก ์จ๊ธฐ๊ณ ์ธ๋ถ์ ์์ฒญ์ ๋ํ ์ ์ ํ ์๋ต์ ํ๋ค. ์ฆ, ํด๋ผ์ด์ธํธ๊ฐ ๋ด๋ถ ๊ตฌ์กฐ๋ฅผ ์ ํ์์์ด ์ฝ์ํ ํํ์ API์ ํต์ ํ๋ ๊ฒ์ด๋ค.
API Gateway์ ๊ธฐ๋ฅ
- ํด๋ผ์ด์ธํธ์ ์์ฒญ์ ์ผ๊ด์ ์ผ๋ก ์ฒ๋ฆฌ
- ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ
- ์ ์ฒด ์์คํ ์ ๋ถํ๋ฅผ ๋ถ์ฐ ์ํค๋ ๋ก๋ ๋ฐธ๋ฐ์์ ์ญํ
- ๋์ผํ ์์ฒญ์ ๋ํ ๋ถํ์ํ ๋ฐ๋ณต ์์ ์ ์ค์ผ ์ ์๋ ์บ์ฑ
- ์์คํ ์์ ์ค๊ณ ๊ฐ๋ ์์ฒญ๊ณผ ์๋ต์ ๋ํ ๋ชจ๋ํฐ๋ง
- ์์คํ ๋ด๋ถ์ ์ํคํ ์ฒ๋ฅผ ์จ๊ธธ ์ ์์
- ํ๋ก์ฐจ๋จ๊ธฐ ๊ธฐ๋ฅ
- ์ธ์ฆ ๋ฐ ์ธ๊ฐ
Spring Cloud์์ MSA๊ฐ ํต์
- RestTemplate
- Feign Client
Netfilx Zuul๋ก ๊ตฌํ
<client - netfilx zull - {service1, service2}>
Spring boot 2.4์์๋ Maintenance ์ํ (๋ ์ด์ ์ง์ํ์ง์๋ ์ํ)
Spring Cloud Gateway
spring cloud gateway - filter
Request, Response Header์ ์๋น์ค์ ๋ง๋ ๊ฐ ์ถ๊ฐ.
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/**
filters:
- AddRequestHeader=first-request, first-request-header2
- AddResponseHeader=first-response, first-response-header2
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-request-header2
- AddResponseHeader=second-response, second-response-header2
cloud.gateway.routes ์ ๋ณด์ ๊ฐ๊ฐ์ ์๋น์ค์ id, uri์ ๋ณด์ ์ถ๊ฐ์ ์ธ path, filter์ ๋ณด๋ฅผ ์ค์ ํด์ค ์ ์๋ค.
FilterConfig.java - ์๋ฐ์ฝ๋๋ฅผ ์ด์ฉํ ํํฐ์ถ๊ฐ
@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")) // ํค๋ ํํฐ
.uri("http://localhost:8081")) // uri์ ๋ณด
.route(r -> r.path("/second-service/**") // ๋ผ์ฐํธ ๋ฑ๋ก
.filters(f -> f.addRequestHeader("second-request", "second-request-header")
.addResponseHeader("second-response", "second-response")) // ํค๋ ํํฐ
.uri("http://localhost:8082")) // uri์ ๋ณด
.build();
// .yml ๋์์ ์๋ฐ์ฝ๋๋ก๋ ๊ฐ๋ฅ
}
}
RouterLocator๋ฅผ ๋ฐํํ๋ FilterConfig๋ฅผ ์คํ๋ง ๋น์ ๋ฑ๋กํด์ ์ฌ์ฉํ ์ ์๋ค.
๋น๋๋ฅผ ์ฌ์ฉํ์ฌ .yml๊ณผ ๊ฐ์ด route์ ๋ณด๋ฅผ ๋ฑ๋กํ๊ณ ํํฐ์ ๋ณด ๋ํ ๋ฑ๋กํ ์ ์๋ค.
CustomFilter - ์ปค์คํ๋ง์ด์ง ํํฐ ์ถ๊ฐ
@Slf4j
@Component
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(CustomFilter.Config.class);
}
@Override
public GatewayFilter apply(CustomFilter.Config config) {
// Custom pre filter. JWT
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom PRE filter: reqeust id -> {}", request.getId());
// Custom Post filter
// ์ฒด์ธ์ exchange ์ ์ฉํ๊ณ ์ ์ฉ์ดํ์ then ์คํ
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Custom PRE filter: response code -> {}", response.getStatusCode());
}));
};
}
public static class Config {
// configration ์ ๋ณด๊ฐ ์๋ค๋ฉด ์ด ํด๋์ค์ ๋ฃ์ด์ฃผ๋ฉด๋๋ค.
}
}
AbstractGatewayFilterFactory์ apply() ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ํ์ฌ ์์ฒญ ์ , ํ๋ก ์ํ๋ ๋์์ด๋ ํํฐ๋ฅผ ์ค์ ํ ์ ์๋ค.
ํด๋น ํด๋์ค๋ฅผ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋กํ๊ณ ๋ฑ๋กํ ๋น์ ์ฌ์ฉํ๋๋ก .yml ํ์ผ์์ ์ค์ ํด์ฃผ๋ฉด๋๋ค.
CustomFilter๋ฅผ ์ ์ฉํ 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/**
filters:
- CustomFilter
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- CustomFilter
Customํํฐ๋ ๋ผ์ฐํฐ๋ง๋ค ์ง์ ํด์ค์ผ๋ง ์ค์ ๋๋ค.
GlobalFilter ์ ์ฉํ๊ธฐ
CustomFilter๋ ๊ฐ๊ฐ์ ๋ผ์ฐํฐ๋ง๋ค ์ง์ ํด์ค์ผ ํ๋ค. ๋ชจ๋ ๋ผ์ฐํฐ์ ํํฐ๋ฅผ ์ ์ฉํด๋ณด์.
@Slf4j
@Component
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter() {
super(GlobalFilter.Config.class);
}
@Override
public GatewayFilter apply(GlobalFilter.Config config) {
// Custom pre filter. JWT
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Global filter baseMessage: reqeust id -> {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("Global Fileter Start: request id -> {}", request.getId());
}
// Custom Post filter
// ์ฒด์ธ์ exchange ์ ์ฉํ๊ณ ์ ์ฉ์ดํ์ then ์คํ
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger())
log.info("Global filter End: response code -> {}", response.getStatusCode());
}));
};
}
@Data
public static class Config {
// configration ์ ๋ณด๊ฐ ์๋ค๋ฉด ์ด ํด๋์ค์ ๋ฃ์ด์ฃผ๋ฉด๋๋ค.
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
ํํฐ๋ฅผ ๋ง๋ค์ด์ฃผ๊ณ .ymlํ์ผ์ ์ค์ ์ ๋ณด๋ฅผ ์์ฑํด์ค๋ค.
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
ํํฐ์ ์ฉ ํ first-service๋ฅผ ํธ์ถํด๋ณด๋ฉด ์๋ ๊ทธ๋ฆผ๊ณผ ๊ฐ์ด Global - Custom - ์คํ - Custom - Global ์์๋ก ๋์จ๋ค.
์ถ๊ฐ์ ์ผ๋ก LoggingFilter๋ฅผ ํน์ ์๋น์ค์๋ง ์ ์ฉํ๊ณ ์ถ๋ค๋ฉด ํํฐ๋ฅผ ๋ฑ๋กํ๊ณ .yml์ ํ์ผ์ ์์ ํด์ฃผ๋ฉด๋๋ค.
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:
# - AddRequestHeader=first-request, first-request-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=second-request, second-request-header2
# - AddResponseHeader=second-response, second-response-header2
- name: CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there
preLogger: true
postLogger: true
Spring Cloud Gateway - Eureka ์ฐ๋
์ง๊ธ๊น์ง ์๋น์ค๋ฅผ ์ ๋ ์นด์ ๋ฑ๋กํ์.
๊ฐ๊ฐ์ ์๋น์ค์ ์ ๋ ์นด ์ ๋ณด๋ฅผ ๋ฃ์ด์ค๋ค.
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
๋ผ์ฐํธ uri ์ค์ ์ uri๊ฐ ์๋ ์ ๋ ์นด์ ๋ฑ๋ก๋ lb์ด๋ฆ์ผ๋ก ์ค์ ํ๋๋ก ํ๋ค.
ํฌํธ๋ฅผ ์ง์ ํ์ง ์์ผ๋ฏ๋ก ๋๋คํ ํฌํธ๊ฐ ์ฌ์ฉ๊ฐ๋ฅํ๊ฒ ๋์๋ค.
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE #http://localhost:8081/ ์ด์ ํฌํธ๋ฅผ ์ง์ ํ์ง ์์
ํด๋ผ์ด์ธํธ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ์ ๋ ์นด์ ๋ฑ๋ก๋ ์๋น์ค๋ฅผ Discoveryํ๊ณ ํด๋นํ๋ ์๋น์ค๋ฅผ ์ฐพ์ ์์ฒญ์ ๋ณด๋ธ๋ค.
๋ก๋ ๋ฐธ๋ฐ์ค ๊ธฐ๋ฅ
๊ธฐ์กด์ first-service์ second-service๋ฅผ ๋๋คํฌํธ๋ก ์คํํ๋๋ก ๋ณ๊ฒฝํด๋ณด์. ๊ทธ๋ฆฌ๊ณ ์ ๋ ์นด์๋ ์ธ์คํด์คid๋ก ์ ์ผํ ๊ฐ์ ๊ฐ์ง๋๋ก ๋ฑ๋กํ์.
๋ณ๊ฒฝ๋ .yml ํ์ผ
server:
port: 0 # random port
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}}
๋ฐ๋ ์๋น์ค์ ์ธ์คํด์ค๋ฅผ ๋ ๊ฐ ์คํํ๊ฒ ๋๋ฉด..
์๋น์ค์ ์ธ์คํด์ค๊ฐ ์คํ๋ ๋ ์ธ์คํด์คid๋ฅผ ๋ง๋ค์ด ์ ๋ ์นด์ ๋ฑ๋กํ๊ฒ ๋๋ค.
์ด์ ํด๋ผ์ด์ธํธ์์ ์์ฒญ์ด ์ค๋ฉด ๋ก๋๋ฐธ๋ฐ์๊ฐ ์์์ ๊ฐ๊ฐ์ ์ธ์คํด์ค์ ์์ฒญ์ ๋ฐฐ๋ถํด์ค๋ค. (๋ก๋๋ฐธ๋ฐ์ฑ)