클라이언트에서 직접 마이크로서비스의 API를 호출하는 대신 API Gateway와만 통신할 수 있도록 하여 마이크로서비스의 주소가 변경되는 등의 상황에 유연하게 대처할 수 있도록 한다.
API Gateway를 사용함으로써 얻게되는 기능은 다음과 같다.
일괄적인 정책 적용, 회로 차단기, QoS 다시 시도 가능
로깅, 마이크로서비스 호출 경로 추적, 상관 관계
Netflix Ribbon
클라이언트 사이드 로드밸런서로, 클라이언트 프로그램 안에서 이동하고자 하는 서비스의 주소값을 관리하는 프로그램이다.
직접 주소를 사용해 API를 호출하는 대신 마이크로서비스의 이름을 통해 API를 호출할 수 있다.
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