본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 21. · 4 Views
Spring Cloud Gateway 완벽 가이드
MSA 환경에서 필수적인 API Gateway 패턴을 Spring Cloud Gateway로 구현하는 방법을 배웁니다. 라우팅, 필터, Predicate 등 핵심 개념을 실무 예제와 함께 쉽게 설명합니다.
목차
1. API Gateway 패턴
어느 날 김개발 씨는 회사에서 새로운 프로젝트에 투입되었습니다. "마이크로서비스 아키텍처로 구성된 시스템이에요." 팀장님의 설명을 들으니 머리가 복잡해졌습니다.
사용자, 주문, 상품, 결제... 수십 개의 서비스가 따로 돌아가고 있었습니다.
API Gateway 패턴은 마이크로서비스 앞단에서 모든 클라이언트 요청을 받아 적절한 서비스로 전달하는 단일 진입점입니다. 마치 대형 쇼핑몰의 안내 데스크처럼, 고객의 질문을 듣고 적절한 매장으로 안내해주는 역할을 합니다.
이를 통해 인증, 로깅, 라우팅 등의 공통 관심사를 한곳에서 처리할 수 있습니다.
다음 코드를 살펴봅시다.
// 기존 MSA 구조 (Gateway 없음)
// 클라이언트가 각 서비스를 직접 호출
http://user-service:8081/api/users
http://order-service:8082/api/orders
http://product-service:8083/api/products
// API Gateway 패턴 적용 후
// 모든 요청이 Gateway를 통과
http://api-gateway:8080/users -> user-service
http://api-gateway:8080/orders -> order-service
http://api-gateway:8080/products -> product-service
김개발 씨는 팀장님께 조심스럽게 물어봤습니다. "클라이언트가 각 서비스를 직접 호출하면 안 되나요?" 팀장님은 웃으며 대답했습니다.
"좋은 질문이에요. 그렇게 하면 어떤 문제가 생길까요?" 먼저 보안 문제가 발생합니다.
모든 내부 서비스를 외부에 노출해야 하기 때문입니다. 만약 30개의 마이크로서비스가 있다면, 30개의 엔드포인트를 모두 관리해야 합니다.
각각에 방화벽 규칙을 설정하고, SSL 인증서를 관리하고, 보안 업데이트를 해야 합니다. 다음으로 인증과 인가의 중복 문제입니다.
모든 서비스에서 JWT 토큰을 검증하는 코드를 작성해야 합니다. 만약 인증 방식이 바뀌면?
30개 서비스를 모두 수정해야 합니다. 상상만 해도 끔찍합니다.
API Gateway 패턴은 이런 문제를 해결하기 위해 탄생했습니다. 쉽게 비유하자면, API Gateway는 마치 대형 백화점의 컨시어지 데스크와 같습니다.
고객이 "화장품 코너는 어디 있나요?"라고 물으면 적절한 층을 안내해줍니다. "이 쿠폰 사용할 수 있나요?"라고 물으면 유효성을 확인해줍니다.
모든 고객이 컨시어지 데스크를 거쳐가면, 백화점은 고객 동선을 파악하고 통계를 낼 수 있습니다. API Gateway도 마찬가지입니다.
모든 클라이언트 요청이 Gateway를 거쳐가면서 다음과 같은 일들이 가능해집니다. 첫째, 인증과 인가를 한곳에서 처리합니다.
JWT 토큰 검증 로직을 Gateway에만 작성하면 됩니다. 내부 마이크로서비스들은 이미 검증된 요청만 받기 때문에 비즈니스 로직에만 집중할 수 있습니다.
둘째, 로깅과 모니터링이 쉬워집니다. 모든 요청이 Gateway를 거쳐가므로, 이곳에서 로그를 수집하면 전체 시스템의 트래픽을 한눈에 파악할 수 있습니다.
"지난주에 주문 API가 몇 번 호출됐지?", "어떤 사용자가 가장 많이 접속했지?" 같은 질문에 즉시 답할 수 있습니다. 셋째, 라우팅 규칙을 유연하게 변경할 수 있습니다.
예를 들어 새 버전의 주문 서비스를 배포했다면, 일부 트래픽만 새 버전으로 보내는 카나리 배포가 가능합니다. 문제가 생기면?
Gateway 설정만 바꾸면 됩니다. 넷째, Rate Limiting과 Circuit Breaker 같은 안정성 패턴을 적용할 수 있습니다.
특정 클라이언트가 초당 100번 이상 요청을 보내면 차단할 수 있습니다. 어떤 서비스가 응답하지 않으면 빠르게 실패하고 대체 응답을 줄 수 있습니다.
실제 현업에서는 어떻게 활용할까요? 쿠팡, 배달의민족 같은 대규모 서비스를 상상해봅시다.
모바일 앱, 웹 브라우저, 파트너 시스템 등 수많은 클라이언트가 동시에 접속합니다. 내부적으로는 사용자 서비스, 상품 서비스, 주문 서비스, 결제 서비스, 배송 서비스 등이 각각 독립적으로 운영됩니다.
만약 Gateway가 없다면? 모바일 앱 개발자는 10개 이상의 서비스 URL을 모두 알아야 합니다.
새 서비스가 추가될 때마다 앱을 업데이트해야 합니다. 서비스 URL이 바뀌면?
재앙입니다. 하지만 Gateway가 있다면 클라이언트는 단 하나의 엔드포인트만 알면 됩니다.
https://api.example.com으로만 요청을 보내면, Gateway가 알아서 적절한 내부 서비스로 전달해줍니다. 주의할 점도 있습니다.
API Gateway는 **단일 장애점(Single Point of Failure)**이 될 수 있습니다. Gateway가 다운되면 전체 시스템이 멈춥니다.
따라서 Gateway를 여러 인스턴스로 구성하고, 로드 밸런서를 앞에 두어야 합니다. 또한 Gateway에 너무 많은 로직을 넣으면 성능 병목이 발생합니다.
모든 요청이 Gateway를 거쳐가기 때문에, Gateway가 느리면 전체 시스템이 느려집니다. 따라서 Gateway는 가볍게 유지해야 합니다.
팀장님의 설명을 듣고 난 김개발 씨는 이해했습니다. "아, 그래서 API Gateway가 필요하군요!" 이제 본격적으로 Spring Cloud Gateway를 배워볼 차례입니다.
API Gateway 패턴은 MSA의 핵심 패턴 중 하나입니다. Netflix, Amazon, Uber 등 글로벌 기업들이 이미 검증한 패턴이며, 여러분의 프로젝트에도 적용할 수 있습니다.
실전 팁
💡 - API Gateway는 인증, 라우팅, 로깅 등 공통 관심사만 처리하고, 비즈니스 로직은 넣지 마세요
- Gateway를 고가용성(HA) 구성으로 배포하여 단일 장애점을 제거하세요
- 응답 시간을 모니터링하여 Gateway가 병목이 되지 않도록 주의하세요
2. Spring Cloud Gateway 소개
김개발 씨는 팀장님께 물었습니다. "API Gateway를 직접 만들어야 하나요?" 팀장님은 고개를 저으며 말했습니다.
"다행히 Spring Cloud Gateway라는 훌륭한 프레임워크가 있어요. 우리는 설정만 하면 됩니다."
Spring Cloud Gateway는 Spring 5, Spring Boot 2, Project Reactor를 기반으로 만들어진 비동기 논블로킹 API Gateway 프레임워크입니다. Zuul 1.x의 후속으로 개발되었으며, Reactive 프로그래밍 모델을 채택하여 높은 성능을 자랑합니다.
Route, Predicate, Filter라는 세 가지 핵심 개념으로 구성됩니다.
다음 코드를 살펴봅시다.
// build.gradle - Spring Cloud Gateway 의존성 추가
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// Spring MVC(Tomcat)가 아닌 WebFlux(Netty)를 사용
}
// Spring Cloud 버전 관리
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.0"
}
}
김개발 씨는 Spring Cloud Gateway에 대해 검색해봤습니다. "왜 Zuul이 아니라 Spring Cloud Gateway를 사용하나요?" 궁금증이 생겼습니다.
Zuul 1.x는 Netflix에서 만든 훌륭한 API Gateway였습니다. 하지만 치명적인 단점이 있었습니다.
블로킹 I/O 방식을 사용한다는 점입니다. 하나의 요청을 처리하는 동안 스레드가 블로킹되기 때문에, 동시 처리 성능에 한계가 있었습니다.
쉽게 비유하자면, Zuul 1.x는 은행 창구와 같습니다. 10개의 창구가 있다면 동시에 10명만 처리할 수 있습니다.
11번째 고객은 기다려야 합니다. 만약 한 고객이 복잡한 업무로 10분이 걸린다면, 그 창구는 10분 동안 다른 고객을 받을 수 없습니다.
반면 Spring Cloud Gateway는 비동기 논블로킹 방식을 사용합니다. Project Reactor를 기반으로 하며, Netty를 웹 서버로 사용합니다.
하나의 스레드가 수천 개의 요청을 동시에 처리할 수 있습니다. 이를 비유하자면 콜센터의 자동 응답 시스템과 같습니다.
한 명의 상담원이 여러 고객의 요청을 동시에 처리합니다. A 고객의 답변을 기다리는 동안 B 고객의 요청을 받고, C 고객에게 답변을 보낼 수 있습니다.
스레드를 블로킹하지 않기 때문에 훨씬 효율적입니다. Spring Cloud Gateway는 세 가지 핵심 개념으로 구성됩니다.
첫째, Route입니다. 라우트는 목적지 URI, Predicate 목록, Filter 목록으로 구성됩니다.
"이런 조건을 만족하는 요청은 저 서비스로 보내라"는 규칙을 정의합니다. 예를 들어 "/api/users"로 시작하는 요청은 사용자 서비스로, "/api/orders"로 시작하는 요청은 주문 서비스로 보내는 식입니다.
둘째, Predicate입니다. Predicate는 요청이 특정 Route에 매칭되는지 판단하는 조건입니다.
Java 8의 Predicate 함수형 인터페이스를 사용합니다. 경로, HTTP 메서드, 헤더, 쿠키 등 다양한 조건을 설정할 수 있습니다.
"만약 요청 경로가 /api/users로 시작하고, HTTP 메서드가 GET이라면"처럼 말이죠. 셋째, Filter입니다.
Filter는 요청이 라우팅되기 전후에 요청과 응답을 수정할 수 있습니다. 인증 헤더를 추가하거나, 응답 시간을 로깅하거나, Rate Limiting을 적용하는 등의 작업을 수행합니다.
Spring MVC의 Interceptor나 Servlet Filter와 비슷한 개념입니다. 실제로 어떻게 동작할까요?
클라이언트가 GET http://api-gateway:8080/api/users/123 요청을 보냈다고 가정해봅시다. Gateway는 먼저 등록된 모든 Route를 순회하며 Predicate를 평가합니다.
"경로가 /api/users로 시작하는가?" 조건을 만족하면 해당 Route가 선택됩니다. 그다음 Pre Filter들이 실행됩니다.
인증 토큰을 검증하고, 요청 로그를 남기고, 필요한 헤더를 추가합니다. 모든 Pre Filter가 통과하면 실제 대상 서비스로 요청을 프록시합니다.
대상 서비스에서 응답이 오면 Post Filter들이 실행됩니다. 응답 시간을 측정하고, 응답 헤더를 추가하고, 로그를 남깁니다.
최종적으로 클라이언트에게 응답을 반환합니다. 주의할 점이 있습니다.
Spring Cloud Gateway는 Spring WebFlux 기반입니다. 따라서 Spring MVC(Tomcat)와 함께 사용할 수 없습니다.
spring-boot-starter-web 의존성이 있으면 충돌이 발생합니다. 반드시 WebFlux만 사용해야 합니다.
또한 Reactive 프로그래밍에 익숙하지 않다면 학습 곡선이 있습니다. Mono, Flux 같은 개념이 낯설 수 있습니다.
하지만 걱정 마세요. Gateway를 사용하는 데 깊은 Reactive 지식이 필요하지는 않습니다.
대부분의 작업은 설정 파일로 해결됩니다. 김개발 씨는 팀장님께 물었습니다.
"그럼 Zuul 2.x는 어떤가요?" 팀장님이 답했습니다. "Zuul 2.x도 비동기 논블로킹을 지원하지만, Spring 생태계와의 통합은 Spring Cloud Gateway가 훨씬 낫죠." 실제로 Spring Cloud Gateway는 Spring Boot의 Auto Configuration, Actuator, 설정 관리 등과 완벽하게 통합됩니다.
Spring Security와의 연동도 자연스럽습니다. Spring을 사용하는 팀이라면 당연한 선택입니다.
성능 측면에서도 검증되었습니다. Netflix, Alibaba 같은 대규모 서비스에서 이미 사용 중이며, 초당 수만 건의 요청을 안정적으로 처리합니다.
Spring Cloud Gateway는 단순히 프록시 역할만 하는 것이 아닙니다. Circuit Breaker, Rate Limiter, Retry 같은 Resilience 패턴을 쉽게 적용할 수 있습니다.
Micrometer를 통한 메트릭 수집도 기본 제공됩니다.
실전 팁
💡 - Spring Cloud Gateway는 WebFlux 기반이므로 Spring MVC와 함께 사용할 수 없습니다
- 높은 동시성이 필요한 경우 논블로킹 방식의 Gateway가 블로킹 방식보다 10배 이상 성능이 좋습니다
- 기존 Zuul 1.x를 사용 중이라면 Spring Cloud Gateway로 마이그레이션을 고려하세요
3. 프로젝트 설정
김개발 씨는 이제 직접 Spring Cloud Gateway 프로젝트를 만들어보기로 했습니다. IntelliJ IDEA를 켜고 Spring Initializr를 실행했습니다.
"어떤 의존성을 추가해야 하지?" 팀장님이 옆에서 조언해줍니다.
Spring Cloud Gateway 프로젝트를 시작하려면 Spring Boot 3.x와 Spring Cloud 2023.x 버전을 사용해야 합니다. 핵심 의존성은 spring-cloud-starter-gateway이며, 이를 추가하면 WebFlux와 Netty가 자동으로 포함됩니다.
Spring MVC 의존성은 절대 포함하지 말아야 합니다.
다음 코드를 살펴봅시다.
// build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java.sourceCompatibility = '17'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2023.0.0")
}
dependencies {
// Gateway 핵심 의존성
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
// 모니터링을 위한 Actuator (선택사항)
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// 테스트 의존성
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
김개발 씨는 Spring Initializr에서 프로젝트를 생성했습니다. 하지만 실행하자마자 에러가 발생했습니다.
"Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway"라는 메시지가 나왔습니다. 팀장님이 웃으며 말했습니다.
"흔한 실수예요. spring-boot-starter-web 의존성을 제거해야 해요." Spring Cloud Gateway의 가장 중요한 제약사항은 Spring MVC와 함께 사용할 수 없다는 점입니다.
Gateway는 Netty 기반의 WebFlux를 사용하는데, Spring MVC는 Tomcat 기반입니다. 둘은 서로 다른 웹 서버이므로 충돌이 발생합니다.
쉽게 비유하자면, 자동차에 휘발유 엔진과 전기 모터를 동시에 장착할 수 없는 것과 같습니다. 하나를 선택해야 합니다.
Gateway를 사용한다면 WebFlux를 선택해야 합니다. 의존성을 올바르게 설정했다면 다음은 애플리케이션 클래스를 만들 차례입니다.
java package com.example.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } } 너무 간단해서 김개발 씨는 의아했습니다. "이게 전부인가요?" 팀장님이 답했습니다.
"네, Gateway의 모든 설정은 application.yml 파일에서 합니다." 이제 application.yml 파일을 작성해봅시다. yaml server: port: 8080 spring: application: name: api-gateway cloud: gateway: # 여기에 라우트 설정이 들어갑니다 routes: [] management: endpoints: web: exposure: include: health,info,metrics,gateway endpoint: gateway: enabled: true 이 설정의 의미를 하나씩 살펴보겠습니다.
server.port: 8080은 Gateway가 8080 포트에서 실행된다는 의미입니다. 클라이언트는 이 포트로 요청을 보냅니다.
spring.application.name은 서비스 이름입니다. Spring Cloud 환경에서는 서비스 간 통신 시 이 이름을 사용합니다.
spring.cloud.gateway.routes는 라우팅 규칙을 정의하는 곳입니다. 지금은 비어있지만, 곧 여기에 라우트를 추가할 것입니다.
management.endpoints는 Spring Boot Actuator 설정입니다. /actuator/gateway/routes 엔드포인트를 통해 현재 등록된 라우트 목록을 조회할 수 있습니다.
운영 환경에서 매우 유용합니다. 실제 프로젝트 구조는 어떻게 될까요?
api-gateway/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/gateway/ │ │ │ ├── ApiGatewayApplication.java │ │ │ ├── filter/ # 커스텀 필터 │ │ │ └── config/ # 추가 설정 │ │ └── resources/ │ │ └── application.yml │ └── test/ │ └── java/ │ └── com/example/gateway/ └── build.gradle 프로젝트를 실행해봅시다. 터미널에서 ./gradlew bootRun을 입력하면 Gateway가 시작됩니다.
로그를 보면 "Netty started on port 8080"이라는 메시지가 나옵니다. Tomcat이 아니라 Netty가 시작된 것을 확인할 수 있습니다.
브라우저에서 http://localhost:8080/actuator/gateway/routes에 접속해보세요. 빈 배열 []이 반환됩니다.
아직 라우트를 등록하지 않았기 때문입니다. 주의할 점이 있습니다.
실제 운영 환경에서는 Actuator 엔드포인트를 외부에 노출하면 안 됩니다. 보안 문제가 발생할 수 있습니다.
Spring Security를 추가하여 인증된 사용자만 접근하도록 설정해야 합니다. 또한 Gateway는 Stateless해야 합니다.
세션을 사용하면 안 됩니다. 여러 Gateway 인스턴스가 로드 밸런싱되는 환경에서는 세션이 공유되지 않기 때문입니다.
인증은 JWT 같은 토큰 기반 방식을 사용해야 합니다. 김개발 씨는 성공적으로 Gateway 프로젝트를 생성했습니다.
"생각보다 간단하네요!" 팀장님이 미소 지으며 말했습니다. "이제 본격적으로 라우트를 정의해봅시다." 프로젝트 설정은 Gateway의 기초입니다.
올바른 의존성을 추가하고, MVC와의 충돌을 피하고, Actuator로 모니터링 준비를 마쳤습니다. 이제 진짜 재미있는 부분이 시작됩니다.
실전 팁
💡 - 절대 spring-boot-starter-web 의존성을 추가하지 마세요. WebFlux와 충돌합니다.
- Actuator의 gateway 엔드포인트를 활성화하면 런타임에 라우트를 조회하고 수정할 수 있습니다.
- 프로덕션 환경에서는 Actuator 엔드포인트에 인증을 반드시 적용하세요.
4. 라우트 정의
김개발 씨는 드디어 첫 번째 라우트를 만들 준비가 되었습니다. "사용자 서비스로 요청을 라우팅하려면 어떻게 해야 하죠?" 팀장님이 application.yml 파일을 열며 설명을 시작합니다.
Route는 Gateway의 핵심 구성 요소로, 고유 ID, 목적지 URI, Predicate 목록, Filter 목록으로 구성됩니다. 라우트는 YAML 설정 파일에서 선언적으로 정의하거나, Java 코드로 프로그래밍 방식으로 정의할 수 있습니다.
각 라우트는 특정 조건을 만족하는 요청을 지정된 서비스로 전달합니다.
다음 코드를 살펴봅시다.
# application.yml - 기본 라우트 정의
spring:
cloud:
gateway:
routes:
# 첫 번째 라우트: 사용자 서비스
- id: user-service-route
uri: http://localhost:8081
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
# 두 번째 라우트: 주문 서비스
- id: order-service-route
uri: http://localhost:8082
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
팀장님이 설명을 시작했습니다. "라우트는 마치 교통 표지판과 같아요.
'서울 방향은 이쪽', '부산 방향은 저쪽'처럼 요청을 적절한 목적지로 안내하는 거죠." 위의 설정을 하나씩 뜯어보겠습니다. 첫 번째 라우트를 보면 id: user-service-route가 있습니다.
이것은 라우트의 고유 식별자입니다. 로그를 볼 때, Actuator로 조회할 때 이 ID로 구분됩니다.
의미 있는 이름을 사용하는 것이 좋습니다. 다음은 uri: http://localhost:8081입니다.
이것은 목적지 서비스의 주소입니다. 조건을 만족하는 요청은 이 URI로 전달됩니다.
실제 운영 환경에서는 로드 밸런서 주소나 서비스 디스커버리 이름을 사용합니다. predicates 섹션은 라우트 매칭 조건입니다.
Path=/api/users/**는 "요청 경로가 /api/users로 시작하면 이 라우트를 선택하라"는 의미입니다. **는 모든 하위 경로를 포함합니다.
/api/users, /api/users/123, /api/users/123/profile 모두 매칭됩니다. 마지막으로 filters 섹션입니다.
StripPrefix=1은 경로의 첫 번째 세그먼트를 제거한다는 의미입니다. 예를 들어 /api/users/123 요청이 들어오면, /users/123로 변환하여 대상 서비스에 전달합니다.
왜 StripPrefix가 필요할까요? 클라이언트는 http://gateway:8080/api/users/123으로 요청합니다.
Gateway는 이를 http://localhost:8081로 프록시합니다. 만약 StripPrefix가 없다면 http://localhost:8081/api/users/123으로 전달됩니다.
하지만 사용자 서비스의 실제 엔드포인트는 /users/123입니다. /api 접두사는 Gateway 레벨에서만 사용하는 것입니다.
따라서 StripPrefix=1을 사용하여 /api를 제거하고 /users/123만 전달합니다. 이렇게 하면 내부 서비스는 Gateway의 존재를 몰라도 됩니다.
실제로 동작하는지 확인해봅시다. 먼저 간단한 사용자 서비스를 8081 포트에서 실행합니다.
그리고 Gateway를 통해 요청을 보냅니다. bash # Gateway를 통한 요청 curl http://localhost:8080/api/users/123 # 실제로는 이렇게 전달됨 # http://localhost:8081/users/123 Gateway의 로그를 보면 라우트 매칭 과정이 출력됩니다.
"Route matched: user-service-route" 같은 메시지를 볼 수 있습니다. Java 코드로도 라우트를 정의할 수 있습니다.
java @Configuration public class GatewayConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("user-service-route", r -> r .path("/api/users/**") .filters(f -> f.stripPrefix(1)) .uri("http://localhost:8081")) .route("order-service-route", r -> r .path("/api/orders/**") .filters(f -> f.stripPrefix(1)) .uri("http://localhost:8082")) .build(); } } YAML 방식과 Java 방식 중 어떤 것을 선택해야 할까요? 대부분의 경우 YAML 방식을 권장합니다.
설정이 명확하고, 재배포 없이 수정할 수 있습니다. Spring Cloud Config와 연동하면 런타임에 라우트를 변경할 수도 있습니다.
하지만 복잡한 로직이 필요하다면 Java 방식이 유리합니다. 예를 들어 데이터베이스에서 라우트를 동적으로 로드하거나, 조건부 라우트를 구성할 때는 코드가 더 편합니다.
실무에서는 어떻게 사용할까요? 보통 환경별로 다른 URI를 설정합니다.
개발 환경에서는 localhost를 사용하지만, 운영 환경에서는 실제 서비스 도메인을 사용합니다. yaml spring: cloud: gateway: routes: - id: user-service-route uri: ${USER_SERVICE_URL:http://localhost:8081} predicates: - Path=/api/users/** 환경 변수 USER_SERVICE_URL을 사용하여 URI를 외부에서 주입받습니다.
Kubernetes 환경이라면 Service 이름을 사용할 수도 있습니다. yaml uri: http://user-service.default.svc.cluster.local:8081 주의할 점이 있습니다.
라우트의 순서가 중요합니다. Gateway는 위에서부터 순서대로 Predicate를 평가하고, 첫 번째로 매칭되는 라우트를 사용합니다.
만약 /api/** 라우트가 /api/users/** 위에 있다면, 모든 요청이 첫 번째 라우트로 가버립니다. 따라서 더 구체적인 라우트를 위에 배치해야 합니다.
/api/users/**를 먼저 쓰고, /api/**를 나중에 쓰는 식입니다. 김개발 씨는 라우트를 정의하고 테스트해봤습니다.
"잘 동작하네요!" 팀장님이 말했습니다. "이제 Predicate와 Filter를 더 깊이 배워봅시다." 라우트 정의는 Gateway의 핵심입니다.
ID, URI, Predicate, Filter를 올바르게 구성하면 복잡한 마이크로서비스 환경을 우아하게 관리할 수 있습니다.
실전 팁
💡 - 라우트 ID는 의미 있는 이름을 사용하세요. 디버깅할 때 도움이 됩니다.
- StripPrefix를 사용하여 Gateway 레벨의 경로 접두사를 제거하세요.
- 구체적인 라우트를 먼저, 일반적인 라우트를 나중에 배치하세요.
5. Predicate와 Filter
김개발 씨는 기본 라우트는 이해했지만, 더 복잡한 조건이 필요했습니다. "특정 헤더가 있을 때만 라우팅하고 싶은데..." 팀장님이 답했습니다.
"그럴 때 Predicate와 Filter를 활용하면 됩니다."
Predicate는 요청이 라우트에 매칭되는지 판단하는 조건으로, Path, Method, Header, Query, Cookie 등 다양한 기준을 제공합니다. Filter는 요청과 응답을 수정하는 컴포넌트로, Pre Filter와 Post Filter로 나뉩니다.
Spring Cloud Gateway는 수십 개의 내장 Predicate와 Filter를 제공합니다.
다음 코드를 살펴봅시다.
# application.yml - 다양한 Predicate와 Filter 활용
spring:
cloud:
gateway:
routes:
- id: advanced-route
uri: http://localhost:8081
predicates:
# 경로 조건
- Path=/api/users/**
# HTTP 메서드 조건
- Method=GET,POST
# 헤더 조건 (정규식 사용 가능)
- Header=X-Request-Id, \d+
# 쿼리 파라미터 조건
- Query=page, \d+
filters:
# 경로 접두사 제거
- StripPrefix=1
# 요청 헤더 추가
- AddRequestHeader=X-Gateway-Name, api-gateway
# 응답 헤더 추가
- AddResponseHeader=X-Response-Time, ${response.time}
# 요청 로깅
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
팀장님이 화이트보드에 그림을 그리며 설명했습니다. "Predicate와 Filter의 관계를 이해하려면 공항 보안 검색대를 떠올리면 됩니다." Predicate는 탑승권 확인과 같습니다.
"이 항공편이 맞나요?", "탑승 시간이 맞나요?", "여권이 유효한가요?" 같은 조건을 확인합니다. 조건을 만족하지 않으면 게이트를 통과할 수 없습니다.
Filter는 보안 검색과 같습니다. 가방을 검사하고, 위험 물품을 제거하고, 필요한 스티커를 붙입니다.
통과한 후에도 면세점에서 쇼핑백을 추가로 받을 수 있습니다. Gateway에서도 마찬가지입니다.
Predicate로 요청을 검증하고, Filter로 요청을 변형합니다. Predicate의 종류를 살펴보겠습니다.
가장 많이 사용하는 것은 Path Predicate입니다. Path=/api/users/**처럼 경로 패턴으로 매칭합니다.
Ant 스타일 패턴을 지원하므로 *, **, ? 같은 와일드카드를 사용할 수 있습니다. Method Predicate는 HTTP 메서드를 검사합니다.
Method=GET,POST처럼 여러 메서드를 지정할 수 있습니다. RESTful API에서 GET 요청만 캐싱하고 싶을 때 유용합니다.
Header Predicate는 요청 헤더를 검사합니다. Header=X-Request-Id, \d+는 "X-Request-Id 헤더가 존재하고, 값이 숫자여야 한다"는 의미입니다.
정규식을 사용할 수 있어서 강력합니다. Query Predicate는 쿼리 파라미터를 검사합니다.
Query=page, \d+는 "page 파라미터가 숫자여야 한다"는 조건입니다. API 버전을 쿼리 파라미터로 관리할 때 활용할 수 있습니다.
Cookie Predicate는 쿠키를 검사합니다. Cookie=sessionId, [a-z0-9]+처럼 사용합니다.
A/B 테스트를 위해 특정 쿠키를 가진 사용자만 새 버전으로 라우팅할 수 있습니다. Host Predicate는 Host 헤더를 검사합니다.
Host=**.example.com처럼 도메인 패턴으로 매칭합니다. 멀티 테넌트 시스템에서 유용합니다.
After/Before/Between Predicate는 시간 기반 라우팅을 제공합니다. 특정 날짜 이후에만 새 버전으로 라우팅하고 싶을 때 사용합니다.
여러 Predicate를 조합하면 더 복잡한 조건을 만들 수 있습니다. yaml predicates: - Path=/api/v2/** - Method=GET - Header=Authorization - After=2024-01-01T00:00:00+09:00[Asia/Seoul] 위 설정은 "경로가 /api/v2로 시작하고, GET 메서드이고, Authorization 헤더가 있고, 2024년 1월 1일 이후"라는 조건을 모두 만족해야 합니다.
조건들은 AND 관계입니다. 이제 Filter를 살펴보겠습니다.
AddRequestHeader Filter는 요청에 헤더를 추가합니다. AddRequestHeader=X-Gateway-Name, api-gateway는 모든 요청에 이 헤더를 추가합니다.
내부 서비스가 "Gateway를 통해 왔구나"라고 인식할 수 있습니다. AddResponseHeader Filter는 응답에 헤더를 추가합니다.
CORS 헤더를 추가하거나, 캐시 정책을 설정할 때 사용합니다. StripPrefix Filter는 앞에서 배웠듯이 경로 접두사를 제거합니다.
StripPrefix=2라면 처음 두 세그먼트를 제거합니다. RewritePath Filter는 경로를 변환합니다.
RewritePath=/api/(?<segment>.*), /$\{segment}처럼 정규식으로 경로를 재작성할 수 있습니다. RedirectTo Filter는 리다이렉트를 수행합니다.
RedirectTo=302, https://example.com처럼 사용합니다. 서비스가 이전되었을 때 유용합니다.
RequestRateLimiter Filter는 Rate Limiting을 적용합니다. Redis를 사용하여 분산 환경에서도 동작합니다.
DDoS 공격을 방어하거나, API 요금제를 적용할 때 필수적입니다. CircuitBreaker Filter는 Circuit Breaker 패턴을 적용합니다.
대상 서비스가 다운되면 빠르게 실패하고 대체 응답을 반환합니다. Filter는 순서가 중요합니다.
Filter는 정의된 순서대로 실행됩니다. 예를 들어 인증 필터는 가장 먼저 실행되어야 합니다.
인증이 실패하면 뒤의 필터를 실행할 필요가 없기 때문입니다. 커스텀 Filter도 만들 수 있습니다.
java @Component public class CustomFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // Pre Filter 로직 ServerHttpRequest request = exchange.getRequest(); System.out.println("Request: " + request.getPath()); return chain.filter(exchange).then(Mono.fromRunnable(() -> { // Post Filter 로직 ServerHttpResponse response = exchange.getResponse(); System.out.println("Response: " + response.getStatusCode()); })); } } 실무에서는 어떻게 활용할까요? 예를 들어 A/B 테스트를 구현한다고 가정해봅시다.
특정 쿠키를 가진 사용자는 새 버전으로, 나머지는 기존 버전으로 라우팅할 수 있습니다. yaml routes: - id: new-version uri: http://new-service:8081 predicates: - Path=/api/users/** - Cookie=version, v2 - id: old-version uri: http://old-service:8081 predicates: - Path=/api/users/** 또는 Blue-Green 배포에도 활용할 수 있습니다.
헤더나 쿼리 파라미터로 트래픽을 분배합니다. 김개발 씨는 Predicate와 Filter의 강력함에 감탄했습니다.
"이걸로 정말 많은 것을 할 수 있겠네요!" 팀장님이 고개를 끄덕였습니다. "맞아요.
Gateway는 단순한 프록시가 아니라 강력한 트래픽 제어 도구입니다." Predicate와 Filter를 잘 활용하면 코드 변경 없이 설정만으로 많은 기능을 구현할 수 있습니다. 이것이 Spring Cloud Gateway의 진정한 힘입니다.
실전 팁
💡 - 여러 Predicate를 조합하여 정밀한 라우팅 조건을 만드세요.
- Filter는 순서가 중요합니다. 인증, 로깅, 변환 순서로 배치하세요.
- Rate Limiter와 Circuit Breaker는 안정성을 위한 필수 Filter입니다.
6. 경로 기반 라우팅
김개발 씨는 실제 프로젝트에 Gateway를 적용하기로 했습니다. 사용자 서비스, 주문 서비스, 상품 서비스가 각각 다른 포트에서 실행 중이었습니다.
"이걸 모두 하나의 엔드포인트로 통합해야 하는데..." 팀장님이 말했습니다. "경로 기반 라우팅을 사용하면 됩니다."
경로 기반 라우팅은 요청 경로에 따라 다른 서비스로 요청을 전달하는 가장 기본적이고 강력한 패턴입니다. /api/users는 사용자 서비스로, /api/orders는 주문 서비스로 라우팅하여 단일 진입점을 제공합니다.
이를 통해 클라이언트는 여러 서비스의 위치를 알 필요가 없어집니다.
다음 코드를 살펴봅시다.
# application.yml - 실전 경로 기반 라우팅 설정
spring:
cloud:
gateway:
routes:
# 사용자 서비스 라우팅
- id: user-service
uri: http://localhost:8081
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Service-Name, user-service
# 주문 서비스 라우팅
- id: order-service
uri: http://localhost:8082
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Service-Name, order-service
# 상품 서비스 라우팅
- id: product-service
uri: http://localhost:8083
predicates:
- Path=/api/products/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Service-Name, product-service
# 정적 리소스는 CDN으로
- id: static-resources
uri: https://cdn.example.com
predicates:
- Path=/static/**
팀장님이 실제 운영 중인 시스템의 아키텍처를 보여줬습니다. 쇼핑몰 시스템이었는데, 10개 이상의 마이크로서비스가 돌아가고 있었습니다.
"과거에는 각 서비스가 별도의 도메인을 가졌어요. user.example.com, order.example.com 이런 식으로요.
문제가 뭐였을까요?" 김개발 씨가 생각해봤습니다. "클라이언트가 모든 도메인을 알아야 하고, CORS 설정도 복잡하고..." 팀장님이 고개를 끄덕였습니다.
"정확해요. 그래서 우리는 경로 기반 라우팅을 도입했습니다." 경로 기반 라우팅은 마치 대형 쇼핑몰의 층별 안내와 같습니다.
1층은 화장품, 2층은 의류, 3층은 가전제품. 고객은 "쇼핑몰"이라는 하나의 입구로 들어와서, 원하는 층으로 이동합니다.
각 매장이 어디 있는지 정확히 몰라도 됩니다. 실제로 어떻게 동작할까요?
클라이언트는 모든 요청을 https://api.example.com으로 보냅니다. Gateway는 경로를 보고 판단합니다.
GET https://api.example.com/api/users/123→ User Service (8081) -POST https://api.example.com/api/orders→ Order Service (8082) -GET https://api.example.com/api/products/456→ Product Service (8083) 클라이언트 입장에서는 마치 하나의 거대한 모놀리식 애플리케이션과 대화하는 것처럼 느껴집니다. 하지만 실제로는 여러 개의 독립적인 서비스가 협력하고 있습니다.
경로 설계는 매우 중요합니다. RESTful 원칙을 따르는 것이 좋습니다.
/api/{resource}/{id} 패턴을 사용하면 직관적입니다. 예를 들어: - /api/users - 사용자 목록 조회 - /api/users/123 - 특정 사용자 조회 - /api/users/123/orders - 특정 사용자의 주문 목록 이렇게 설계하면 Gateway 라우팅 규칙도 간단해집니다.
버전 관리도 경로에 포함시킬 수 있습니다. yaml routes: - id: user-service-v1 uri: http://user-service-v1:8081 predicates: - Path=/api/v1/users/** - id: user-service-v2 uri: http://user-service-v2:8081 predicates: - Path=/api/v2/users/** 이렇게 하면 구버전과 신버전을 동시에 운영할 수 있습니다.
클라이언트는 자신이 원하는 버전을 선택할 수 있습니다. 마이크로 프론트엔드와도 통합할 수 있습니다.
yaml routes: # API 요청 - id: api-gateway uri: http://backend-services predicates: - Path=/api/** # 프론트엔드 - 사용자 관리 페이지 - id: frontend-users uri: http://frontend-users:3001 predicates: - Path=/users/** # 프론트엔드 - 주문 관리 페이지 - id: frontend-orders uri: http://frontend-orders:3002 predicates: - Path=/orders/** 이렇게 하면 프론트엔드와 백엔드를 모두 Gateway로 통합할 수 있습니다. 실무에서 자주 발생하는 문제를 해결해봅시다.
문제 1: 서비스 간 호출 주문 서비스가 사용자 서비스를 호출해야 한다면? Gateway를 거쳐야 할까요?
아니면 직접 호출해야 할까요? 일반적으로 내부 서비스 간 호출은 직접 하는 것이 좋습니다.
Gateway는 외부 클라이언트를 위한 것입니다. 내부 호출까지 Gateway를 거치면 성능이 저하되고, Gateway가 단일 장애점이 됩니다.
대신 Service Discovery(Eureka, Consul 등)를 사용하여 서비스를 찾습니다. 문제 2: 경로 충돌 만약 /api/users/orders와 /api/orders가 모두 주문 서비스로 가야 한다면?
이런 경우 더 구체적인 경로를 먼저 배치합니다. yaml routes: - id: user-orders # 더 구체적 - 먼저 uri: http://order-service:8082 predicates: - Path=/api/users/*/orders/** - id: orders # 더 일반적 - 나중 uri: http://order-service:8082 predicates: - Path=/api/orders/** 문제 3: 인증이 필요한 경로 일부 경로는 인증이 필요하고, 일부는 공개라면?
Filter에서 조건부로 처리하거나, 라우트를 분리합니다. yaml routes: - id: public-users uri: http://user-service:8081 predicates: - Path=/api/users/register - Method=POST # 인증 필터 없음 - id: protected-users uri: http://user-service:8081 predicates: - Path=/api/users/** filters: - name: AuthenticationFilter # 커스텀 인증 필터 김개발 씨는 실제로 경로 기반 라우팅을 적용해봤습니다.
모바일 앱 개발자가 기뻐하며 말했습니다. "이제 하나의 URL만 알면 되네요!
훨씬 편해졌어요." 경로 기반 라우팅은 단순하지만 강력합니다. 마이크로서비스의 복잡성을 숨기고, 클라이언트에게 깔끔한 인터페이스를 제공합니다.
팀장님이 마지막으로 조언했습니다. "경로 설계는 한 번 정하면 바꾸기 어려워요.
처음부터 신중하게 설계하세요. RESTful 원칙을 따르고, 일관성 있게 작성하세요." 김개발 씨는 고개를 끄덕였습니다.
Spring Cloud Gateway를 통해 MSA의 핵심 패턴을 배웠습니다. 이제 자신 있게 실무에 적용할 수 있을 것 같습니다.
API Gateway는 단순한 프록시가 아닙니다. 보안, 모니터링, 트래픽 제어, 서비스 통합의 중심입니다.
Spring Cloud Gateway는 이 모든 것을 우아하게 해결해줍니다.
실전 팁
💡 - 경로 설계는 RESTful 원칙을 따르고, 일관성 있게 작성하세요.
- 더 구체적인 경로를 먼저, 일반적인 경로를 나중에 배치하세요.
- 내부 서비스 간 호출은 Gateway를 거치지 말고 직접 호출하세요.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
관찰 가능한 마이크로서비스 완벽 가이드
마이크로서비스 환경에서 시스템의 상태를 실시간으로 관찰하고 모니터링하는 방법을 배웁니다. Resilience4j, Zipkin, Prometheus, Grafana, EFK 스택을 활용하여 안정적이고 관찰 가능한 시스템을 구축하는 실전 가이드입니다.
Prometheus 메트릭 수집 완벽 가이드
Spring Boot 애플리케이션의 메트릭을 Prometheus로 수집하고 모니터링하는 방법을 배웁니다. Actuator 설정부터 PromQL 쿼리까지 실무에 필요한 모든 내용을 다룹니다.
스프링 관찰 가능성 완벽 가이드
Spring Boot 3.x의 Observation API를 활용한 애플리케이션 모니터링과 추적 방법을 초급 개발자 눈높이에서 쉽게 설명합니다. 실무에서 바로 적용할 수 있는 메트릭 수집과 분산 추적 기법을 다룹니다.
Zipkin으로 추적 시각화 완벽 가이드
마이크로서비스 환경에서 분산 추적을 시각화하는 Zipkin의 핵심 개념과 활용 방법을 초급자도 쉽게 이해할 수 있도록 실무 스토리로 풀어낸 가이드입니다. Docker 실행부터 UI 분석까지 단계별로 배웁니다.
Micrometer Tracing 완벽 가이드
분산 시스템에서 요청 흐름을 추적하는 Micrometer Tracing의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 실무 스토리와 비유로 풀어낸 완벽 가이드입니다.