본문 바로가기

MicroService Architecture

API Gateway 라이브러리 선택 및 사용 예제

API Gateway 대표 라이브러리

 

  • Netflix Zuul
  • Spring Cloud Gateway

 

Netflix Zuul

  • 넷플릭스 OSS(오픈소스 소프트웨어) 스택의 일부로 개발된 API Gateway
  • 주로 MSA에서 사용되며 다양한 트래픽 라우팅, 필터링 및 로드 밸런싱 기능을 제공

 

Netflix Zuul의 특징

  • 1세대 API Gateway
    •  Zuul은 기본적으로 HTTP 요청 라우팅과 필터링에 중점을 둔 1세대 API Gateway
    • Spring Cloud에서 Zuul 1을 사용하는 경우, 내부적으로 블로킹 방식으로 동작
  • 필터 기반 아키텍처
    • Zuul은 요청을 처리하는 여러 필터를 지원
    • 필터를 사용해 요청 전/후의 다양한 기능(인증, 로깅, 트랜잭션 관리 등)을 추가 가능
  • 확장성
    • Zuul은 기능 확장성이 뛰어나지만 JVM 기반으로 블로킹 I/O 모델을 사용하기 때문에 고성능이 필요한 대규모 시스템에는 성능이 제한될 수 있음
  • 넷플릭스 OSS 지원 종료
    • 넷플릭스는 Zuul 1에 대한 공식 지원을 종료
    • 따라서 유지보수와 새로운 기능 추가가 제한적
    • 현재는 Zuul 2로의 전환을 권장하지만 Zuul 2는 Spring Cloud와의 완전한 통합이 이루어지지 않아 사용하기가 어려움

Spring Cloud Gateway

  • Spring Cloud Gateway는 스프링 생태계를 위한 API Gateway로 Zuul의 단점을 개선비동기, 논블로킹 API Gateway
  • 스프링 5와 리액티브 프로그래밍을 기반으로 개발

 

Spring Cloud Gateway 특징

  • 비동기 및 논블로킹
    • Spring Cloud Gateway는 Netty 기반으로 설계되어 비동기 논블로킹 방식으로 동작
    • 이를 통해 대규모 트래픽을 효율적으로 처리 가능
  • Spring과의 완벽한 통합
    • Spring Cloud Gateway는 Spring Boot와 Spring Cloud와 완벽히 통합
    • Spring 생태계를 사용하는 개발자들에게 친숙한 설정 방식과 코드 구조를 제공
  • 라우팅 및 필터 기능
    • 다양한 필터(사전/사후 필터)를 지원하며, 라우팅과 필터 로직을 간편하게 설정 가능
    • 예를 들어, 경로 기반 라우팅, 헤더 기반 라우팅, 권한 검증 등을 손쉽게 구성 가능
  • 향후 지원 및 커뮤니티
    • Spring 프로젝트는 활발히 유지보수되고 있으며, 새로운 기능과 보안 패치가 지속적으로 제공
    • 또한, 넷플릭스 OSS의 많은 기능들이 Spring Cloud로 대체되었기 때문에 향후 유지보수 측면에서도 안정적

라이브러리 선택

결론적으로는 Spring Cloud Gateway를 사용하는 것을 권장

  1. 비동기 및 고성능 처리
    • Spring Cloud Gateway는 비동기 논블로킹 구조로 설계되어 있어 성능이 뛰어나고 대규모 트래픽 처리에 유리
  2. Spring 생태계와의 완벽한 통합
    • Spring Boot 및 Spring Cloud를 사용하는 프로젝트에서는 Spring Cloud Gateway를 통해 간편하게 API Gateway를 구성 가능
  3. 커뮤니티 지원 및 유지보수
    • Spring Cloud Gateway는 지속적으로 개선되고 있으며, 넷플릭스 Zuul 1은 더 이상 공식 지원이 없는 상태

결론

  • Zuul을 고려할 수 있는 경우기존에 넷플릭스 OSS 스택을 사용 중이거나 특정 비즈니스 요구사항 때문에 Zuul의 필터 아키텍처를 선호하는 경우
  • 그러나 신규 프로젝트나 장기적인 관점에서는 Spring Cloud Gateway가 더 적합한 선택
블록킹I/O는 요청이 발생하면 해당 요청이 끝날 때까지 스레드가 기다리는 방식으로 응답이 올 때까지 스레드가 차단(Block) 되는 방식이기 때문에 비효율적인 자원 사용, 스레드 수의 한계, 컨텍스트 스위칭 오버헤드를 이유로 문제가 발생한다고 함


추후 블록킹 I/O, 논블록킹 I/O, Context Switching에 대해 더 알아보도록 할 것

 


API Gateway 사용 예제

1. API Gateway로 사용할 모듈 생성 및 settings.gradle 추가

rootProject.name = 'playheaven'

include 'member-service'
include 'order-service'
include 'game-service'
include 'eureka-server'
include 'api-gateway'

2. 의존성 추가

  • API Gateway 또한 Eureka Client로 등록해서 Intstance 정보들을 Fetch 받아야 서비스의 정보(IP주소, 포트)가 변경되거나 새로운 인스턴스가 추가 되었을 때 자동으로 인식할 뿐만 아니라, Eureka Server에서 가지고 온 서비스 인스턴스 목록을 사용하여 로드 밸런싱을 해줄 수 있음
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'

주의사항
  • IntelliJ Starters에서 Gateway를 검색하면 Spring Cloud Routing에 Gateway와 Reactive Gateway 두 가지가 나옴

  • 반드시 Reactive Gateway를 선택해야 하고 그냥 Gateway 의존성을 추가하면 아래 처럼 끝에 mvc가 붙게 됨
  • 그렇게 되면 아래 사진 처럼, spring.cloude.gateway.routes를 인식할 수 없게 되기 때문에 주의
implementation 'org.springframework.cloud:spring-cloud-starter-gateway-mvc'

 

3. Eureka Client 활성화 (작성하지 않아도 자동 인식되긴 함)

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

 

4. 라우팅 설정 (api-gateway의 application.yml파일)

server:
  port: 9999

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: member-service
          uri: lb://member-service
          predicates:
            - Path=/member-service/**
          filters:
            - StripPrefix=1

        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order-service/**
          filters:
            - StripPrefix=1

        - id: game-service
          uri: lb://game-service
          predicates:
            - Path=/game-service/**
          filters:
            - StripPrefix=1

eureka:
  instance:
    prefer-ip-address: true
  client:
    register-with-eureka: true
    fetch-registry: true
    serviceUrl:
      defaultZone: http://localhost:8888/eureka
  • spring.cloud.gateway.routes.id
    • 라우팅을 구분하기 위한 route id를 지정
    • spring.application.name에 지정한 것과 관계 없음
    • 각 라우트에 대해 고유한 id를 지정함으로써 등록, 업데이트 ,삭제 등의 관리에 사용
  • spring.cloud.gateway.routes.uri
    • 요청이 라우팅되는 경로 URI를 의미
    • lb 프로토콜(lb://)의 경로를 지정하면 Eureka Server에서 호스트에 해당하는 서비스를 찾고 로드밸런싱을 수행
    • Ex) lb://member-service라고 한다면, routes.id가 아니라 spring.application.name이 member-serviceEureka Server에 등록된 서비스를 찾아 로드밸런싱
  • spring.cloud.gateway.routes.predicates
    • 요청 URI를 기반으로 라우팅 조건을 정의
      예를 들어, Path=/member-service/**/member-service/로 시작하는 모든 요청을 이 라우트로 매핑  
    • Postman 요청: http://아이피주소:APIGateway포트/member-service/api/member/list라는 요청이 들어오면, 경로가 /member-service/**로 설정된 라우트가 이 요청을 처리
    • 매핑: MemberController의 @RequestMapping("/api/member")로 정의된 엔드포인트와 매칭
  • spring.cloud.gateway.routes.filters.StripPrefix
    • 요청 URL의 시작부분부터 지정된 수의 경로 세그먼트를 제거하는 접두사 제거 명령
    • 요청 URI : http://localhost:8080/member-service/api/member/list
    • 필터 설정 : StripPrefix=1
    • 필터 적용 후
    • 변경된 URI : http://localhost:8080/api/member/list
    • member-service 제거, 나머지 경로가 백엔드 서비스로 전달
@RequestMapping("/api/member")
public class MemberController {

 

주의 사항
  • StripPrefix=1 사이에 공백이 있으면 에러 발생
  • java.lang.IllegalArgumentException: Unable to find GatewayFilterFactory with name StripPrefix
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: member-service
          uri: lb://member-service
          predicates:
            - Path=/member-service/**
          filters:
            - StripPrefix = 1

 

5. Postman 테스트 및 결과

@RestController
@RequestMapping("/api/mypage/wishlist")
@RequiredArgsConstructor
public class WishlistController {
    private final WishlistService wishlistService;
    @GetMapping("/list/{memberId}")
    public ResponseEntity<WishlistResponseDto> list(@PathVariable(name = "memberId") Long memberId,
                                                    @RequestParam(name = "pageNo", defaultValue = "1") int pageNo,
                                                    @RequestParam(name = "size",defaultValue = "10")int size){                                                   
}