API Gateway
클라이언트에서 직접 마이크로서비스의 API를 호출하는 대신 API Gateway와만 통신할 수 있도록 하여 마이크로서비스의 주소가 변경되는 등의 상황에 유연하게 대처할 수 있도록 한다.
API Gateway를 사용함으로써 얻게되는 기능은 다음과 같다.
인증 및 권한 부여
마이크로서비스 검색 통합
응답 캐싱
일괄적인 정책 적용, 회로 차단기, QoS 다시 시도 가능
속도 제한
로드밸런싱
로깅, 마이크로서비스 호출 경로 추적, 상관 관계
헤더, 쿼리 문자열 등 요청 데이터 변환
IP 허용 목록을 기반으로 요청 처리
Netflix Ribbon
클라이언트 사이드 로드밸런서로, 클라이언트 프로그램 안에서 이동하고자 하는 서비스의 주소값을 관리하는 프로그램이다.
비동기 처리가 잘 지원되지 않는다.
직접 주소를 사용해 API를 호출하는 대신 마이크로서비스의 이름을 통해 API를 호출할 수 있다.
health check가 가능하다.
Spring Boot 2.4부터 더이상 사용할 수 없는 maintainance 상태이다. 대신 Spring Cloud Loadbalancer를 사용하면 된다.
Netflix Zuul
Routing, API gateway 역할을 한다.
Spring Boot 2.4부터 더이상 사용할 수 없는 maintainance 상태이다. 대신 Spring Cloud Gateway를 사용하면 된다.
예제
아래와 같이 아주 간단한 컨트롤러를 가진 WAS를 두 대 구동하여 eureka에 등록한다. First Service, Second Service를 각각 8081, 8082 포트에 구동하면 된다.
이 때 eureka client 의존성을 추가해주어야 한다.
@RestController
@RequestMapping("/")
public class BasicController {
@GetMapping("/welcome")
public String welcome() {
return "This is First Service.";
}
}
server:
port: 8081
spring:
application:
name: my-first-service
eureka:
client:
register-with-eureka: false
fetchRegistry: false
그리고 Zuul Service를 위한 어플리케이션을 구현한다. 이를 통해
localhost:8000/first-service/welcome
으로 접속하면localhost:8081/welcome
과 동일한 결과가 반환된다.
@SpringBootApplication
@EnableZuulProxy
public class ZuulServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServiceApplication.class, args);
}
}
server:
port: 8000
zuul:
routes:
first-service:
path: /first-service/**
url: http://localhost:8081
second-service:
path: /second-service/**
url: http://localhost:8082
인증 서비스, 로깅 등의 비즈니스 로직을 수행하기 위해 필터를 등록할 수 있다. 아래는 로깅을 하기 위한 Zuul 필터의 예제이다.
@Component
public class ZuulLoggingFilter extends ZuulFilter {
Logger logger = LoggerFactory.getLogger(ZuulLoggingFilter.class);
@Override
public Object run() throws ZuulException {
logger.info("************ printing logs: ");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
logger.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
네티 기반의 웹서버로 구현되어 있어 비동기 처리가 가능하다.

예제
앞서 Zuul 예제와 동일하게 두 대의 WAS를 띄워둔다.
그리고 api gateway를 위한 새로운 애플리케이션을 구현한다.
아래와 같이 yaml 파일을 작성하거나 RouteLocator 빈을 등록하여 라우트 정보를 설정할 수 있다.
Zuul과 달리
localhost:8000/first-service/welcome
으로 접속하면localhost:8081/first-service/welcome
URI가 호출된다. 이를 변경하려면 RewritePath 필터를 추가하면 된다.필터를 등록하여 헤더를 추가하거나 원하는 로직을 작성할 수 있다.
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
# Spring Cloud Gateway 설정
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, header1
- AddResponseHeader=first-response, header2
#
- RewritePath=/first-service/(?<segment>.*), /$\{segment}
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, header1
- AddResponseHeader=second-response, header2
@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", "header1")
.addResponseHeader("first-response", "header2"))
.uri("http://localhost:8081/"))
.route(r -> r.path("/second-service/**")
.filters(f -> f.addRequestHeader("second-request", "header1")
.addResponseHeader("second-response", "header2"))
.uri("http://localhost:8082/"))
.build();
}
}
커스텀 필터
커스텀 필터를 아래와 같이 구현할 수 있다. 이후, 커스텀 필터를 Route에 등록해주어야 한다.
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
ServerHttpResponse res = exchange.getResponse();
log.info("Custom PRE filter: request uri -> {}", request.getId());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Custom POST filter: response code -> {}", response.getStatusCode());
}));
};
}
public static class Config {
// put the configuration properties
}
}
spring:
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- 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 req = exchange.getRequest();
ServerHttpResponse res = exchange.getResponse();
log.info("Global Filter base message: {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("Global Filter Start: request id -> {}", request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPreLogger()) {
log.info("Global Filter End: response code -> {}", response.getStatusCode());
}
}));
};
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
글로벌 필터를 아래와 같이 등록할 수 있다. 매개변수를 설정하여 필터 내부의 Config 객체에 이 매개변수 값을 매핑하기 때문에 필터에서 값을 사용할 수 있다.
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
필터 순서 제어
필터의 적용 순서를 제어하기 위해 아래와 같이 OrderedGatewayFilter 객체를 생성해 반환할 수 있다.
HIGHEST_PRECEDENCE일 경우 필터가 가장 바깥쪽에 위치하게 되며, LOWEST_PRECEDENCE일 경우 필터가 가장 안쪽에 위치하게 된다.
@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 req = exchange.getRequest();
ServerHttpResponse res = exchange.getResponse();
log.info("Global Filter base message: {}", config.getBaseMessage());
if (config.isPreLogger()) {
log.info("Global Filter Start: request id -> {}", request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPreLogger()) {
log.info("Global Filter End: response code -> {}", response.getStatusCode());
}
}));
}, Ordered.LOWEST_PRECEDENCE);
}
로드밸런서
API gateway는 eureka 서버에 어떤 마이크로 서비스를 호출해야 할 지 확인한 후 해당 마이크로 서비스로 요청을 보내게 된다.
이 때 로드밸런싱을 하기 위해서는 라우트 정보에 직접 호스트와 포트 정보를 지정하는 대신, 서비스 이름을 지정하여 해당 서비스 이름을 가진 여러 WAS에 요청을 분산해 보낼 수 있도록 한다.
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
# Spring Cloud Gateway 설정
cloud:
gateway:
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
- id: second-service
uri: lb://MY-SECOND-SERVICE
predicates:
- Path=/second-service/**
Last updated