[LG CNS AM 6기] 37일차 TIL : Spring Cloud Gateway로 MSA의 단일 진입점 구성하고 Filter·Eureka·Load Balancer 연결하기

윤현일·5일 전

LGCNS

목록 보기
42/43

1. 오늘의 한 줄 요약

Spring Cloud Gateway를 이용해 클라이언트 요청을 각 마이크로서비스로 전달하고, 요청과 응답에 Filter를 적용한 뒤 Eureka에 등록된 여러 서비스 인스턴스로 요청을 분산하는 과정을 실습했다.

지난 시간에 Eureka를 이용해 서비스의 위치를 등록했다면, 오늘은 API Gateway가 Eureka에서 서비스를 찾고 적절한 인스턴스로 요청을 전달하도록 연결했다.

2. 배운 내용

API Gateway란?

MSA에서는 회원, 주문, 상품과 같은 기능이 서로 다른 서비스로 나뉘어 실행된다.

클라이언트가 각 서비스의 IP와 포트를 직접 알아야 한다면 서비스가 추가되거나 주소가 변경될 때마다 클라이언트도 수정해야 한다.

API Gateway는 여러 마이크로서비스 앞에 위치하는 단일 진입점이다.

Client
  │
  ▼
API Gateway
  ├─ First Service
  ├─ Second Service
  └─ 그 외 Microservice

클라이언트는 API Gateway의 주소만 알고 있으면 된다. Gateway는 요청 경로를 확인해 적절한 서비스로 요청을 전달한다.

API Gateway에는 다음과 같은 공통 기능도 적용할 수 있다.

  • 요청 경로에 따른 라우팅
  • 인증과 권한 확인
  • 요청 및 응답 로깅
  • 헤더 추가 및 변경
  • 속도 제한
  • 서비스 검색
  • 부하 분산
  • 장애 처리

각 서비스가 인증이나 로깅 같은 공통 기능을 반복해서 구현하지 않고, Gateway에서 먼저 처리할 수 있다는 장점이 있다.

Spring Cloud Gateway와 WebFlux

오늘 실습에서는 WebFlux 기반의 Spring Cloud Gateway를 사용했다.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>
        spring-cloud-starter-gateway-server-webflux
    </artifactId>
</dependency>

WebFlux 기반 Gateway는 Reactor를 사용하는 비동기 방식으로 요청을 처리한다. 필터 코드에서도 Mono와 chain.filter(exchange)가 사용된 것을 확인할 수 있었다.

기존 Netflix Zuul과 Ribbon은 유지보수 단계에 들어간 기술이며, 오늘 코드는 Spring Cloud Gateway와 Spring Cloud LoadBalancer를 이용하는 방식으로 구성했다.

3. 실습 / 적용

Gateway Route 설정

API Gateway는 8000번 포트에서 실행했다.

server:
  port: 8000

클라이언트 요청은 Gateway의 Path Predicate에 따라 First Service 또는 Second Service로 전달된다.

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: first-service
              uri: lb://MY-FIRST-SERVICE
              predicates:
                - Path=/first-service/**
              filters:
                - CustomFilter

            - id: second-service
              uri: lb://MY-SECOND-SERVICE
              predicates:
                - Path=/second-service/**
              filters:
                - CustomFilter
                - name: LoggingFilter

예를 들어 다음 요청은 First Service로 전달된다.

http://localhost:8000/first-service/welcome

다음 요청은 Second Service로 전달된다.

http://localhost:8000/second-service/welcome

여기서 Route를 구성하는 주요 요소는 다음과 같다.

  • id: Route를 구분하는 이름
  • uri: 요청을 전달할 서비스
  • predicates: 어떤 요청을 해당 Route가 처리할지 결정하는 조건
  • filters: 요청과 응답을 전달하는 과정에서 실행할 작업

Pre Filter와 Post Filter

Gateway Filter는 요청이 실제 서비스로 전달되기 전과 응답이 클라이언트에게 돌아가기 전에 공통 작업을 수행할 수 있다.

Client
  ↓
Pre Filter
  ↓
Microservice
  ↓
Post Filter
  ↓
Client

오늘 작성한 Filter에서는 다음과 같이 요청과 응답 정보를 기록했다.

return chain.filter(exchange)
    .then(Mono.fromRunnable(() -> {
        log.info(
            "response code -> {}",
            response.getStatusCode()
        );
    }));

chain.filter(exchange)를 호출하면 다음 필터 또는 실제 서비스로 요청이 전달된다. 이후 응답이 돌아오면 then() 내부의 Post Filter 로직이 실행된다.

Custom Filter

CustomFilter는 특정 Route에만 적용하는 필터다.

First Service와 Second Service Route에 각각 설정했으며 요청이 들어올 때 Request ID를, 응답이 돌아올 때 Response Status를 기록했다.

Custom Pre Filter
→ 실제 서비스 호출
→ Custom Post Filter

Route마다 필요한 인증 검사, 헤더 추가, 로깅 등의 작업을 Custom Filter로 분리할 수 있다.

Global Filter

GlobalFilter는 Gateway를 통과하는 모든 Route에 공통으로 적용했다.

default-filters:
  - name: GlobalFilter
    args:
      baseMessage: Spring Cloud Gateway Webflux Global Filter
      preLogger: true
      postLogger: true

baseMessage, preLogger, postLogger 값을 설정 객체로 전달하고, 옵션에 따라 요청과 응답 로그를 출력하도록 구성했다.

공통 인증이나 전체 요청 로깅처럼 모든 서비스에 필요한 기능은 Global Filter에 작성할 수 있다.

Logging Filter와 실행 순서

Second Service에는 별도의 LoggingFilter도 적용했다.

OrderedGatewayFilter와 Ordered.HIGHEST_PRECEDENCE를 이용해 필터의 우선순위를 지정했다.

실행 로그를 통해 필터가 다음과 같은 순서로 동작한다는 것을 확인할 수 있다.

요청
→ Global Pre Filter
→ Custom Pre Filter
→ Logging Pre Filter
→ Second Service
→ Logging Post Filter
→ Custom Post Filter
→ Global Post Filter
→ 응답

요청이 들어갈 때는 설정된 필터 순서대로 실행되고, 응답은 반대 방향으로 필터를 통과한다.

Eureka와 Gateway 연결

처음에는 Gateway가 다음과 같이 서비스의 주소를 직접 가지고 있었다.

uri: http://localhost:8081

하지만 이 방식은 서비스의 포트나 실행 위치가 변경될 때마다 Gateway 설정도 수정해야 한다.

오늘은 Gateway와 각 서비스를 Eureka Client로 등록하고, 고정 주소 대신 서비스 이름을 사용했다.

uri: lb://MY-FIRST-SERVICE
uri: lb://MY-SECOND-SERVICE

lb://는 Eureka에서 해당 이름의 서비스를 검색하고, 등록된 인스턴스 중 하나를 선택해 요청을 전달하겠다는 의미다.

Client
  │
  ▼
API Gateway
  │
  ├─ Eureka에 MY-FIRST-SERVICE 위치 요청
  │
  └─ Eureka에 MY-SECOND-SERVICE 위치 요청
          │
          ▼
   등록된 Instance로 요청 전달

이제 Gateway는 First Service와 Second Service의 실제 포트를 직접 알 필요가 없다.

여러 인스턴스로 요청 분산하기

First Service와 Second Service는 다음과 같이 랜덤 포트를 사용하도록 설정했다.

server:
  port: 0

각 인스턴스를 Eureka에서 구분할 수 있도록 고유한 instance-id도 설정했다.

eureka:
  instance:
    instance-id: >
      ${spring.cloud.client.ip-address}:
      ${spring.application.instance_id:${random.value}}

같은 서비스를 여러 개 실행하면 Eureka에는 같은 서비스 이름 아래 여러 인스턴스가 등록된다.

MY-FIRST-SERVICE
├─ Instance A : Random Port
└─ Instance B : Random Port

MY-SECOND-SERVICE
├─ Instance A : Random Port
└─ Instance B : Random Port

클라이언트가 Gateway에 같은 주소로 반복 요청하면 Gateway는 Eureka에서 인스턴스 목록을 확인하고 요청을 분산한다.

각 서비스의 /check API에서는 실제 실행 포트를 응답하도록 작성해, 요청할 때마다 서로 다른 인스턴스가 선택되는지 확인할 수 있었다.

4. 문제와 해결

오늘 실습에서는 큰 오류 없이 API Gateway와 Eureka를 연결했다.

다만 서비스 주소를 localhost:8081과 같이 직접 지정하면 여러 인스턴스를 실행하더라도 특정 인스턴스에만 요청이 전달된다.

이를 해결하기 위해 다음과 같이 구조를 변경했다.

고정 주소
http://localhost:8081
        ↓
서비스 이름
lb://MY-FIRST-SERVICE

Gateway가 Eureka를 통해 서비스의 위치를 찾도록 변경하면서, 서비스의 실제 포트와 관계없이 요청을 전달하고 여러 인스턴스에 부하를 분산할 수 있게 되었다.

5. 다음에 할 일

  • Gateway Filter에서 인증 토큰을 확인하는 방법 알아보기
  • Gateway와 각 서비스의 요청 로그를 연결해 추적하는 방법 알아보기
  • 서비스 인스턴스가 종료됐을 때 Eureka와 Gateway가 처리하는 과정 확인하기
profile
개발자

0개의 댓글