[Spring-cloud] SpringBoot MSA를 위한 Api Gateway
in Spring on Spring
Api Gateway란?
Spring Boot MSA(Microservices Architecture)를 위한 API Gateway는 마이크로서비스 아키텍처에서 중요한 역할을 하는 컴포넌트이다. API Gateway는 클라이언트와 백엔드 마이크로서비스 간의 중간 계층으로 작동하며, 단일 진입점 제공, 요청 라우팅, 인증 및 인가, 로드 밸런싱, 데이터 변환, 모니터링 및 로깅, 캐싱 등의 기능을 수행한다. 이를 통해 시스템의 보안, 성능, 가용성을 향상시키고, 클라이언트와 마이크로서비스 간의 상호작용을 간소화한다. Spring Boot에서는 주로 Spring Cloud Gateway를 사용하여 API Gateway를 구현한다.
의존성
- Spring Initializr > Spring Cloud Routing > Gateway
dependencies {
// Spring Boot Starter Web
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring Cloud Starter Gateway
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
// Spring Boot Starter Actuator (Optional, for monitoring)
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
yml을 통한 기본 설정
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/**
yml대신 자바 config로 설정 할 수 있다.
@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-by-java")
.addResponseHeader("first-response", "first-response-header-by-java")
)
.uri("http://localhost:8081"))
.route(r -> r.path("/second-service/**")
.filters(f -> f.addRequestHeader("second-request", "second-request-header-by-java")
.addResponseHeader("second-response", "second-response-header-by-java"))
.uri("http://localhost:8082"))
.build();
}
}
필터 역시 yml로 설정 할 수 있다.
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/**
커스텀 필터
더 다양한 옵션을 위해 커스텀 필터를 구현 할 수 있다.
@Component
@Slf4j
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
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Custom POST filter: response code -> {}", response.getStatusCode());
}));
};
}
public static class Config {
// Put the configuration properties
}
}
- yml에 커스텀필터를 등록해 준다.
...
filters:
- AddRequestHeader=first-request, first-request-header2
- AddResponseHeader=first-response, first-response-header2
- CustomFilter
...
전체 적용을 위한 글로벌 필터를 구현할 수 있다.
@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(), request.getRemoteAddress());
if (config.isPreLogger()) {
log.info("Global Filter Start: request id -> {}", request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(()->{
if (config.isPostLogger()) {
log.info("Global Filter End: response code -> {}", response.getStatusCode());
}
}));
});
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
- yml
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
커스텀 필터를 활용한 로그필터 구현
@Component
@Slf4j
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());
}
return chain.filter(exchange).then(Mono.fromRunnable(()->{
if (config.isPostLogger()) {
log.info("Logging POST Filter: response code -> {}", response.getStatusCode());
}
}));
}, Ordered.HIGHEST_PRECEDENCE); // 실행 순서를 정할 수 있다.
return filter;
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
- yml
...
filters:
- AddRequestHeader=first-request, first-request-header2
- AddResponseHeader=first-response, first-response-header2
- CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there.
preLogger: true
postLogger: true
...
Eureka서버와 Gatway연동
사용자는 최초로 게이트웨이에 접속하게 되며, 게이트웨이는 유레카 서버에서 해당 서비스에 필요한 서버 정보를 취득한다. 이제 게이트웨이의 로드 밸런싱 설정에 대해 알아보자.
- Gatway 설정
cloud:
gateway:
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, first-request-header2
- AddResponseHeader=first-response, first-response-header2
- CustomFilter
- service 설정
server:
port: 0
spring:
application:
name: my-first-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
instance:
instance-id: ${spring.cloud.client.ip-address}:${spring.application.instance_id:${random.value}}