API Gateway : Spring Cloud Gateway
Spring Cloud Gateway 선택 이유
일단 아래와 같은 요구사항을 기반으로 Spring Cloud Gateway를 선택했다.
- Spring Boot 3 이상에서 동작해야함
- Spring에서 공식 지원해야함
- WebSocket와 같은 장기 연결을 지원해야함
이러한 상황에서 Spring Boot에서 공식 지원을 중산했고, 장기 연결을 지원하지 않는 Zuul보다는 Spring Cloud Gateway가 적절하다고 판단했다.
Spring Cloud Gateway의 기능
Spring Cloud Gatway는 Spring Webflux 기반의 API Gateway이다.
또한, Webflux 기반이기에 Netty(웹 서버) 기반의 비동기 처리를 지원한다.
Spring Cloud Gateway는 아래와 같은 기능을 제공할 수 있다.
- 전역적인 Security와 필터 설정
- route 단위의 전처리/후처리 필터 설정
- route 단위의 라우팅 설정
여기서 route는 요청을 적절한 경로로 전달하기 위한 spring cloud gateway의 기본 구성 단위이다.
route는 아래와 같은 요소들로 구성된다.
- id : route의 식별자
- uri : 클라이언트 요청의 최종 목적지
- predicate : 클랑이언트 요청이 특정 조건에 만족하는지 검사하는 함수
- filter : 요청이 uri로 전달되는 과정에서의 전처리/후처리 필터
Spring Cloud Gateway의 동작 방식
Spring Cloud Gateway는 아래와 같은 방식으로 동작한다

- 클라이언트가 spring cloud gateway에 요청을 보냄
- Gateway Handler Mapping이 각각의 route에 요청을 매핑
- predicate는 해당 요청이 route에 대한 조건에 부합하는지 검사
- 만약 predicate에 부합하다면 pre filter에서 요청을 전처리 후 service로 요청 전달
- service의 응답을 post filter가 후처리 후 client에게 응답 전달
Service Discovery : Netflix Eureka
Netflix Eureka 기능
Service Disvoery는 MSA 환경에서 서비스의 주소 정보를 관리하는 역할을 한다.
이를 통해 클라이언트는 동적인 값인 서비스의 주소 정보를 Service Discovery로부터 받아올 수 있다.
Netflix Eureka는 아래와 같은 기능을 제공한다.
- 서비스 주소 정보를 서비스 이름과 같은 임의의 값으로 대체 가능
- 기본값은 application name이지만, 임의의 값을 지정할 수 있다.
- 동일한 서비스가 여러개가 있다면 Ribbon 기반의 자체 로드밸런싱 기능 제공
- lb://{서비스 이름} 으로 요청을 보내면 로드밸런싱 기능을 제공한다
- Service Discovery에 저장된 서비스들의 주소 정보를 eureka와 연결된 다른 서비스와 공유 가능
Neflix Eureka 동작 방식
Netflix Eureka는 Eureka Client와 Eureka Server로 나뉜다.
- Eureka Client : Service Registry에 서비스 정보를 전달 및 받아오는 주체
- 각각의 서비스들을 eureka client로 등록할 수 있고 아래와 같은 대표적인 옵션을 제공한다.
- fetch-registry : Eureka Server로부터 Service Registry의 값을 받아올지의 여부
- register-with-eureka : Service Registry에 현재 서비스의 정보를 등록할지 여부
- service-url : Eureka Server의 주소
- Eureka Server : 서비스 주소 정보를 관리하는 서버
- enable-self-preservation : client가 헬스 체크 기준에 부합하지 않을 때, 해당 인스턴스를 만료시킬지 여부
- eureka server는 service registry에 등록할 이유가 없기에 fetch-registry와 register-with-eureka를 false 처래했다.
이러한 client와 server는 아래와 같이 동작한다.
- eureka client 시작시 eureka server에 자신의 인스턴스 정보를 전달
- eureka client는 일정 주기로 heart beat를 보내서 정상적으로 동작하고 있음을 알린다.
- 일정 주기로 service registry를 client에 공유하여 여러 인스턴스 간 인스턴스 정보를 동기화한다.
MSA 구축 전 기본 설정하기
초기 서비스 구조와 빌드 스크립트
MSA 아키텍처를 구축하기 전에 초기 서비스는 아래와 같은 구조로 되어있을 것이다.
.
├── build.gradle
├── gradle
│ └── wrapper
├── gradlew
├── gradlew.bat
├── settings.gradle
├── src
현재 서비스 구조는 모놀로식 아키텍처로 되어 있고, MSA에서는 여러개의 모듈을 추가하여 각 서비스들을 관리한다.
- build.gradle : gradle 프로젝트의 빌드 스크립트이다
- 모든 gradle 프로젝트는 하나 이상의 빌드 스크립트를 갖는다.
- 빌드 스크립트에는 빌드 구성, 작업, 플러그인, 의존성 등이 들어간다.
- settings.gradle : 모든 gradle 프로젝트의 시작점
- 현재 프로젝트의 빌드에 하위 프로젝트를 추가할 수 있는 파일
- gradle, gradlew : gradle 서비스 빌드를 위한 도구
- src : 서비스 코드가 들어있는 디렉토리
또한, Spring initalizer로 서비스를 생성했을 때, 아래와 같은 build.gardle이 생성된다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.msa'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// 의존성을 추가했다면, 의존성이 들어있을 것이다.
}
우리는 build.gradle과 settings.gradle을 수정하여 루트 경로를 msa 서비스들을 관리하는 루트 프로젝트로 만들 것이다.
루트 프로젝트로 수정하기
우선 루트 경로의 build.gradle을 아래와 같이 수정한다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.3'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.msa'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
bootJar {
enabled = false
}
jar {
enabled = false
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2024.0.0")
}
subprojects {
group = 'com.msa'
// plugin
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
// repository
repositories {
mavenCentral()
}
// java version
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
// plain jar 생성 방지
jar {
enabled=false
}
// spring boot jar 생성
bootJar{
enabled=true
}
// spring cloud dependency management
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
// public dependencies
dependencies {
// 필요하면 추가
}
}
크게 다섯가지 변화가 있다.
- 루트 경로의 src 폴더 삭제 : 루트 프로젝트는 각 msa를 관리하는 역할만 하기에 프로젝트 코드는 필요 없다
- bootJar.enabled = false : 루트 프로젝트에 대한 spring boot jar 파일 생성을 막는다.
- 이는 빌드 과정에서 루트 경로에 대한 메인 클래스를 찾는 것을 방지하여 오류를 예방한다.
- jar.enabled = false : 루트 프로젝트에 대한 class jar 파일 생성을 막는다.
- bootJar.enabled = false와 마찬가지의 이유이다.
- ext : springCloudVersion 관리를 위한 환경 변수를 설정한다.
- subProjects : 루트 모듈의 하위 모듈들에 공통적으로 적용할 설정을 기술한다.
- plugins, repositories, dependencies 등을 기술한다.
- 하위 프로젝트는 spring boot jar를 생성해야 하므로 bootJar.enabled=ture이다.
- 다만, 공통 기능 모듈을 제외하면 java class에 대한 jar 파일은 생성할 필요 없기에 jar.enabled=false이다.
주의할 점은, ext의 SpringCloudVersion은 Spring Boot 버전에 따라서 따로 지정해야 한다는 것이다.
아래 사이트에서 적절한 버전을 찾아서 선택해야 한다.
https://spring.io/projects/spring-cloud
Spring Cloud
Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, short lived microservices and
spring.io
이 외 세부적인 여러 변화들이 있지만, 이들을 모두 설명하지는 않겠다. 나머지는 직접 찾아보자.
Spring Cloud Gateway 구축하기
하위 프로젝트 생성 및 추가
우선 아래와 같이 gateway를 위한 하위 모듈을 추가한다.
디렉터리와 파일을 생성하는 것이 아닌 gradle 프로젝트를 생성하는 것이다.
.
├── build.gradle
├── gateway-service
│ ├── build.gradle
│ └── src
│ ├── main
│ └── test
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
이제 생성한 모듈을 루트 프로젝트의 하위 프로젝트로 등록할 것이다.
루트 경로의 settings.gradle을 아래와 같이 수정한다. (아마 자동으로 될 수도 있다)
rootProject.name = '내 루트프로젝트 이름'
include 'gateway-service'
서비스 의존성 준비하기
우선 gateway service의 build.gradle에 아래와 같은 의존성들을 추가한다.
dependencies {
// webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// eureka
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// cloud gateway
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
// test
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
- spring-boot-starter-webflux : Spring Cloud Gateway는 Netty 기반으로 동작하므로 Webflux를 따로 추가했다.
- spring-cloud-starter-gateway : Spring Cloud Gateway에 대한 의존성
- spring-cloud-starter-netflix-eureka-client : Spring Cloud Gateway를 eureka client로 추가하기 위한 의존성
메인 클래스 생성하기
다음으로 서비스의 Main 클래스에 @SpringBootApplication을 추가한다.
필자는 파일과 클래스 이름을 GatewayServiceApplication으로 수정하여 사용핬다.
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
}
@EnableDiscoveryClient 어노테이션은 현재 인스턴스를 eureka client로 등록하기 위한 설정이다.
Gateway Route 설정하기
다음으로 Gateway Route 설정을 진행한다.
application.yml로 쉽게 설정할 수 있지만, 이번 프로젝트에서는 따로 코드로 작성해보기로 했다.
@Configuration
public class RouteLocatorConfig {
private String userServiceUrl = "lb://USER-SERVICE";
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/users/**").uri(userServiceUrl))
.build();
}
}
위 코드는 현재 내가 진행하고 있는 프로젝트의 코드를 일부 가져온 것이다.
현재 프로젝트에는 user service와 eureka 설정이 없기에 정상 동작하지 않을 가능성이 크다.
위 코드가 어떻게 동작하는지만 알아두고 각자 사정에 맞게 추후 수정하자.
- RouteLocator : Spring Cloud Gateway가 관리하는 route에 대해서 기술한다.
- routes().route(id, predicateSpec).uri(service-uri) : 각 라우트에 대해서 기술한다
- id : 라우트의 식별자
- predicateSpec : 요청이 조건에 부합하는지 확인하기 위한 함수를 기술한다.
- uri : 요청이 predicateSpec에 부합할 때, 해당 요청을 전달할 서비스의 URI를 기술한다.
따라서 위 코드는 /users/**에 대한 요청이 들어왔을 때, user service로 요청을 전달하는 역할을 한다.
eureka client 설정하기
마지막으로 application.yml와 메인 클래스를 수정하여 gateway service에 대한 eureka client 설정을 진행한다.
아래와 같이 application.yml을 수정한다.
server:
port: 8080
spring:
application:
name: gateway-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
이를 통해 아래 효과를 얻는다.
- gateway는 localhost:8080에서 동작
- eureka는 spring.application.name을 서비스 이름(주소의 대체값)으로 사용한다.
- eureka client 설정을 진행한다.
- register-with-eureka와 fetch-registry를 true로 설정하여 클라이언트 정보를 받아오고 등록할 수 있도록 한다.
- service-url.defaultZone을 통해 유레카 서버 주소를 등록한다. ( {유레카 서버 주소}/eureka/ )
이를 통해 cloud gateway와 eureka client에 대한 전반적인 설정을 완료했다.
Netflix Eureka Server 구축하기
discovery service 생성 및 의존성 준비
eureka server도 spring cloud gateway와 같이 하나의 모듈로 관리한다.
아래와 같은 파일 구조가 되도록 discovery service 프로젝트를 생성한다.
.
├── build.gradle
├── discovery-service
│ ├── build.gradle
│ └── src
│ ├── main
│ └── test
├── gateway-service
│ ├── build.gradle
│ └── src
│ ├── main
│ └── test
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
마찬가지로 discovery service도 settings.gradle에 추가해야 한다 (자동으로 될 수도 있음)
다음으로 discovery service의 build.gradle에 아래와 같은 의존성들을 추가한다.
dependencies {
// eureka-server
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
// test
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
해당 의존성은 eureka server를 활성화하기 위한 의존성이다.
메인 클래스 생성 및 application.yml 수정
필자는 main 클래스 이름을 DiscoveryServiceApplication으로 수정했고 아래와 같이 작성했다.
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServiceApplication.class, args);
}
}
이는 현재 인스턴스를 eureka server로 사용한다는 것을 의미한다.
다음으로 application.yml을 아래와 같이 생성한다.
# server config
server:
port: 8761
# spring application config
spring:
application:
name: discovery-service
# eureka config
eureka:
server:
enable-self-preservation: true
client:
register-with-eureka: false
fetch-registry: false
이를 통해 아래 효과를 얻는다.
- eureka server는 localhost:8761에서 동작한다.
- enable-self-preservation=true을 통해 heart beat에 문제가 있어도 인스턴스를 만료시키지 않는다
- eureka server는 레지스트리를 fetch할 필요도, 등록될 필요도 없기에 기존 client 설정은 모두 false로 지정한다.
최종 점검
마지막으로 eureka server와 gateway service를 실행하여 eureka server에 gateway service가 정상적으로 등록되는지 확인한다.
gateway와 discovery 인스턴스 실행 후 http://localhost:8761에 접속하여 gateway service가 등록되었는지 확인하면 된다.

정상적으로 GATEWAY-SERVICE가 등록된 것을 확인할 수 있다.
마무리 : Eureka에서 발생할 수 있는 보안 문제
이번에는 MSA의 핵심 요소 중 하나인 gateway와 service discovery에 대해서 알아보았다.
마지막으로 eureka에 대한 의문증과 해결 과정에 대해 설명하고 마무리하겠다.
eureka의 보안 문제
eureka 실습을 통해 볼 때, eureka client로의 등록은 eureka server의 주소만 있으면 가능하기에 보안상 취약하다
eureka client로 등록되지 않는다면 lb://{service name}을 통해 요청을 보낼수는 없지만, eureka client로 등록되기만 한다면 가능해지기 때문에 이는 큰 문제가 될 수 있다.
이를 해결하기 위해서 Api Gateway를 통한 네트워크 분리라는 개념을 활용할 수 있다.
Api Gateway를 통한 네트워크 분리
이 개념은 간단하게 설명하자면, 두 가지 특징을 갖는다.
- 외부 네트워크는 내부 네트워크와 완벽하게 독립된 공간이다.
- 내부 서비스들은 API Gateway를 통해서만 요청을 처리할 수 있다.
- 서비스들은 API Gateway를 통해서만 외부와 통신할 수 있다. (즉 API Gateway는 단일 진입점이다)
- 외부 사용자는 API Gateway를 통해서만 내부 서비스와 통신할 수 있기에 Gateway의 보안 기능을 통해 내부 서비스를 보호할 수 있다.
결과적으로, API Gateway를 통해 보안성을 높이고, 네트워크를 구조적으로 분리하며, 서비스 간 통신을 효율적으로 관리할 수 있다.
'Spring > MSA' 카테고리의 다른 글
| Spring Boot MSA 아키텍처 구축 : 기초 개념 (0) | 2025.03.08 |
|---|