Home Spring Cloud Gateway 구축 및 Eureka 연동
Post
Cancel

Spring Cloud Gateway 구축 및 Eureka 연동

기존에는 각 서비스를 직접 호출하거나 Nginx로 라우팅했는데, 서비스가 늘어날수록 관리가 어려워져서 Spring Cloud Gateway를 도입했다. Eureka와 연동해서 서비스가 자동으로 라우팅되고 로드밸런싱까지 되게 구성했다.


전체 구조

1
2
3
4
5
6
7
8
클라이언트
    │
    ▼
[Gateway :8080]  ──Eureka 조회──▶  [Eureka :8761]
    │                                     │
    │ lb://data-api                       │ 등록된 인스턴스 정보
    ▼                                     │
[data-api :8081] ◀────────────────────────┘

Gateway는 Eureka에서 서비스 목록을 주기적으로 받아온다. 클라이언트가 /data-api/**로 요청하면 Eureka에서 data-api 서비스의 실제 인스턴스 주소를 찾아 로드밸런싱해서 전달한다.


build.gradle

Spring Boot 3.0.11, Spring Cloud 2022.0.4 기준이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.11'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.gateway'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', '2022.0.4')
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

jar {
    enabled = false
}

Spring Cloud Gateway는 WebFlux 기반이라 spring-boot-starter-web이 들어가면 충돌한다. web 의존성은 추가하지 않는다.


application-dev.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
server:
  port: 8080

spring:
  application:
    name: platform-be-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true              # Eureka 등록 서비스 자동 라우팅
          lower-case-service-id: true  # /data-api처럼 소문자 경로 사용
          filters:
            - StripPrefix=0          # context-path 보존 (아래 설명 참고)

      globalcors:
        cors-configurations:
          '[/**]':
            allowedOriginPatterns:
              - http://localhost:3000
              - https://example.com
            allowedMethods:
              - "*"
            allowedHeaders:
              - "*"
            allowCredentials: true
            maxAge: 3600

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

application-prd.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server:
  port: 8080

spring:
  application:
    name: platform-be-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
          filters:
            - StripPrefix=0

      globalcors:
        cors-configurations:
          '[/**]':
            allowedOriginPatterns:
              - https://example.com
            allowedMethods:
              - "*"
            allowedHeaders:
              - "*"
            allowCredentials: true
            maxAge: 3600

eureka:
  client:
    service-url:
      defaultZone: http://${EUREKA_HOSTNAME:localhost}:8761/eureka/
  instance:
    prefer-ip-address: true

Discovery Locator 동작 방식

discovery.locator.enabled: true를 설정하면 수동으로 라우트를 작성하지 않아도 된다. Eureka에 등록된 서비스를 자동으로 발견해서 라우트를 생성한다.

1
2
3
Eureka에 "data-api" 등록
    ↓
Gateway가 자동으로 /data-api/** → lb://data-api 라우트 생성

새 서비스가 추가될 때 Gateway 설정을 건드릴 필요 없이, 새 서비스가 Eureka에 등록만 하면 자동으로 라우팅된다.


StripPrefix 이슈

Discovery Locator는 기본적으로 StripPrefix=1을 적용한다. 서비스 ID 경로를 제거하고 전달하는 것인데, 서비스에 context-path가 설정되어 있으면 문제가 된다.

예시: platform-data-api의 context-path가 /data-api인 경우

 경로
클라이언트 요청/data-api/sems/api/inference/store-risk
StripPrefix=1 (기본)/data-api 제거 → /sems/api/inference/store-risk 전달 → 404
StripPrefix=0 (수정)그대로 → /data-api/sems/api/inference/store-risk 전달 → 200

라우팅 결정(어느 서비스로 갈지)은 여전히 첫 번째 경로 세그먼트(/data-api)로 한다. StripPrefix는 그 이후 백엔드에 전달할 경로만 제어한다.

1
2
3
4
5
/data-api/sems/api/...
    ↓ 첫 세그먼트로 data-api 서비스 식별
lb://data-api 로 라우팅
    ↓ StripPrefix=0이므로 경로 그대로 전달
http://실제IP:8081/data-api/sems/api/...  ← context-path와 일치

요청 로깅 필터

어떤 서비스의 어떤 API를 호출했는지 Gateway 레벨에서 로그를 남긴다. 요청/응답 바디는 남기지 않고 메타정보만 기록한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Slf4j
@Component
public class LoggingGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long startTime = System.currentTimeMillis();
        ServerHttpRequest request = exchange.getRequest();

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            long elapsed = System.currentTimeMillis() - startTime;
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            HttpStatusCode status = exchange.getResponse().getStatusCode();

            log.info("[GATEWAY] {} {} → [{}] {} | status={} | {}ms",
                    request.getMethod(),
                    request.getURI().getPath(),
                    route != null ? route.getId() : "unknown",
                    route != null ? route.getUri() : "unknown",
                    status != null ? status.value() : "-",
                    elapsed
            );
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

로그 출력 예시:

1
[GATEWAY] GET /data-api/sems/api/inference/store-risk → [ReactiveCompositeDiscoveryClient_data-api] lb://data-api | status=200 | 142ms

인증 처리 방침

Gateway에서 토큰 검증을 하고 하위 서비스로 사용자 정보를 헤더로 넘기는 방식도 고려했으나, 웹 API와 모바일 API의 인증 방식이 달라서 Gateway에서 통일된 검증이 어려웠다. 결국 각 서비스가 직접 인증을 처리하고, Gateway는 순수하게 라우팅과 로깅만 담당하도록 했다.

Gateway는 AuthToken 헤더를 그대로 하위 서비스에 전달한다.


정리

설정설명
discovery.locator.enabled: trueEureka 등록 서비스 자동 라우팅
lower-case-service-id: true서비스 ID를 소문자 URL 경로로 사용
filters: StripPrefix=0context-path 있는 서비스 라우팅 시 필수
globalcorsWebFlux 기반 Gateway의 CORS 설정
prefer-ip-address: truehostname 대신 IP로 Eureka 등록
This post is licensed under CC BY 4.0 by the author.