[Spring-cloud] SpringBoot MSA를 위한 Api Gateway

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}}

© 2023 Lee. All rights reserved.