[Spring boot] Spring cloud gateway - intro - 1

brandon·2025년 5월 30일

spring-boot

목록 보기
8/15

Spring cloud gateway?

Spring cloud gateway는 Spring boot, Spring webflux, Reactor를 사용하여 만들어진 Spring 진영의 Api gateway 프레임워크이다.

Spring webflux를 사용하므로 높은 처리량을 보여주며, Spring Circuit Breaker도 지원하고 있어서 높은 안정성을 보장한다.

Spring webflux?

Spring Webflux는 Reactive Programming을 지원하는 Spring Framework의 모듈 중 하나다. Reactive Programming은 비동기적인 데이터 처리를 통해 더 높은 성능과 효율성을 제공하는 프로그래밍 패러다임이다.

Spring Webflux는 이러한 Reactive Programming 패러다임을 기반으로 하며, Non-Blocking I/O 작업을 수행하는데 적합한 Web Application을 개발할 수 있게 도와준다.

Spring Framework 5.0에서 도입된 리액티브 스택(reactive-stack) 웹 프레임워크.

Reactive stack

이는 "리액티브 프로그래밍"이라는 패러다임을 기반으로 합니다. 리액티브 프로그래밍은 비동기 데이터 스트림 (data streams)변화의 전파 (propagation of change)에 중점을 둡니다. 전통적인 request-per-thread 모델 대신, WebFlux는 적은 수의 이벤트 루프 스레드가 많은 수의 요청을 처리하는 리액티브 접근 방식을 채택합니다.

데이터 스트림 Data streams

리액티브 프로그래밍에서 데이터 스트림은 시간에 따라 발생하는 값, 이벤트 또는 오류의 순차적인 흐름을 나타냅니다. 전통적인 프로그래밍에서 변수가 특정 시점에 하나의 값을 가지는 것과 달리, 스트림은 시간의 흐름에 따라 여러 값을 '방출(emit)'할 수 있습니다.

데이터 스트림은 다음과 같은 특징을 가집니다:

비동기적(Asynchronous): 스트림의 데이터는 언제든지, 어떤 순서로든 발생할 수 있습니다. 이는 I/O 이벤트, 사용자 입력, 네트워크 요청 결과 등 다양한 비동기적인 상황을 모델링하는 데 적합합니다.

연속적(Continuous): 많은 경우 스트림은 끝이 없는(infinite) 데이터 흐름을 나타냅니다. 예를 들어, 마우스 클릭 이벤트 스트림은 프로그램이 실행되는 동안 계속해서 발생할 수 있습니다. 물론, 파일 읽기처럼 유한한 스트림도 존재합니다.

관찰 가능(Observable): 스트림은 'Observable'이라고도 불립니다. 이는 스트림을 '관찰(observe)'하거나 '구독(subscribe)'하여 스트림이 방출하는 데이터를 받을 수 있다는 의미입니다.

다양한 종류:

  • 사용자 인터페이스 이벤트: 마우스 클릭, 키 입력, 드래그 앤 드롭 등
    데이터베이스 변경: 데이터베이스의 특정 테이블에 대한 변경 사항
  • 네트워크 요청: API 호출의 응답
  • 시간 기반 이벤트: 타이머, 주기적인 데이터 업데이트
  • 컬렉션: 배열이나 리스트와 같은 정적인 데이터도 스트림으로 변환하여 처리할 수 있습니다.

리액티브 프로그래밍에서는 이 데이터 스트림을 중심으로 모든 작업을 수행합니다. 즉, 데이터를 변수처럼 다루는 대신, 스트림으로 간주하고 스트림에 대한 연산(operators)을 적용하여 데이터를 변환, 결합, 필터링합니다.

변화의 전파 Propagation of Change

변화의 전파는 리액티브 프로그래밍의 핵심 메커니즘으로, 한 데이터 스트림의 값이 변경될 때, 이 스트림에 의존하는 다른 모든 스트림이나 컴포넌트들이 자동으로 업데이트되도록 하는 방식입니다. 이는 마치 스프레드시트에서 한 셀의 값이 변경되면, 그 셀을 참조하는 모든 셀의 값이 자동으로 재계산되는 것과 유사합니다.

변화의 전파는 주로 다음과 같은 방식으로 이루어집니다:

옵저버 패턴(Observer Pattern)의 확장: 리액티브 프로그래밍은 옵저버 패턴을 기반으로 합니다. 'Observable'(데이터 스트림)은 자신에게 '구독(subscribe)'한 여러 'Observer'(관찰자 또는 소비자)에게 데이터 변화를 알립니다.

선언적(Declarative) 방식: 개발자는 데이터가 어떻게 흐르고 어떤 변환을 거칠지 '선언'합니다. 데이터를 명시적으로 가져오고 처리하는 명령형(Imperative) 방식과 달리, 리액티브 프로그래밍은 데이터 흐름의 파이프라인을 정의하고, 데이터가 파이프라인에 주입되면 자동으로 처리되도록 합니다.

명령형 코드


var numbers = [1, 2, 3, 4, 5];
var result = [];

for (var i = 0; i < numbers.length; i++) { // 어떻게 반복할지 (how)
  if (numbers[i] % 2 === 0) {             // 어떻게 짝수인지 검사할지 (how)
    result.push(numbers[i] * 2);          // 어떻게 2를 곱해서 새 리스트에 추가할지 (how)
  }
}

console.log(result); // [4, 8]

선언적 코드

const numbers = [1, 2, 3, 4, 5];

const result = numbers
  .filter(number => number % 2 === 0) // 무엇: 짝수만 필터링해줘 (what)
  .map(number => number * 2);        // 무엇: 각 숫자에 2를 곱해줘 (what)

console.log(result); // [4, 8]

연산자(Operators): 리액티브 라이브러리(RxJava, Project Reactor, RxJS 등)는 map, filter, reduce, merge, zip 등 다양한 연산자를 제공합니다. 이러한 연산자들은 스트림을 변환하거나 결합하여 새로운 스트림을 생성합니다. 중요한 점은, 원본 스트림에 새로운 값이 방출되면, 이러한 연산자들이 자동으로 적용되어 그 변화가 파이프라인을 따라 '전파'된다는 것입니다.

비동기 처리와 백프레셔(Backpressure): 변화의 전파는 비동기적으로 발생합니다. 즉, 데이터를 생성하는 속도가 데이터를 소비하는 속도보다 빠를 때, 소비자가 과부하되지 않도록 '백프레셔' 메커니즘을 통해 데이터 흐름을 제어할 수 있습니다. 소비자가 생산자에게 자신이 처리할 수 있는 데이터의 양을 요청합니다. 예를 들어, "나에게 지금 10개의 데이터만 보내줘"라고 요청하는 식입니다. 생산자는 이 요청을 받으면, 요청된 양만큼만 데이터를 보내고, 소비자가 추가 요청을 하기 전까지는 더 이상 데이터를 보내지 않습니다. 이는 시스템의 안정성과 효율성을 높이는 데 중요합니다.

자동 업데이트: 변화가 발생하면, 해당 스트림을 구독하고 있는 모든 소비자(UI 컴포넌트, 다른 비즈니스 로직 등)는 자동으로 최신 데이터를 받아서 업데이트됩니다. 이는 개발자가 수동으로 데이터 변경을 감지하고 업데이트 로직을 작성해야 하는 부담을 줄여줍니다.

Reactive Programming의 의의?

리액티브 프로그래밍이 등장하고 인기를 얻게 된 배경은 주로 현대 소프트웨어 개발의 요구사항 변화와 전통적인 프로그래밍 패러다임의 한계점 때문입니다.

멀티코어 프로세서의 보급과 병렬 처리의 필요성 증대:

과거에는 CPU 클럭 속도를 높이는 방식으로 성능을 향상했지만, 물리적 한계에 부딪혔습니다. 대신 코어 수를 늘리는 멀티코어 아키텍처가 일반화되었습니다.
이러한 멀티코어 환경의 이점을 제대로 활용하려면 병렬 처리(Parallel Processing)동시성(Concurrency)을 효율적으로 관리해야 합니다. 전통적인 스레드와 락(lock) 기반의 동시성 제어는 복잡하고 오류 발생 가능성이 높습니다.
리액티브 프로그래밍은 비동기적이고 논블로킹(Non-blocking) 방식으로 작업을 처리함으로써, 적은 수의 스레드로도 많은 요청을 효율적으로 처리하여 멀티코어 환경의 이점을 최대한 활용할 수 있게 합니다.

분산 시스템과 네트워크 기반 애플리케이션의 증가:

마이크로서비스 아키텍처, 클라우드 컴퓨팅의 확산으로 애플리케이션이 여러 서비스로 분리되고 네트워크를 통해 통신하는 분산 시스템이 보편화되었습니다.
네트워크 지연(latency)은 불가피하며, 많은 I/O 작업(데이터베이스 쿼리, 외부 API 호출 등)은 본질적으로 비동기적이고 시간이 오래 걸립니다.
이러한 I/O 작업들이 스레드를 블로킹(Blocking)하면 시스템 전체의 처리량(throughput)이 급격히 저하됩니다. 리액티브 프로그래밍은 이러한 논블로킹 I/O를 효과적으로 다루어 시스템의 응답성과 확장성을 크게 향상시킵니다.

사용자 인터페이스(UI)의 반응성 요구:

웹 애플리케이션, 모바일 앱 등 현대 UI는 사용자의 입력(클릭, 스크롤 등)에 즉각적으로 반응해야 합니다.
오래 걸리는 백그라운드 작업이 UI 스레드를 블로킹하면 UI가 멈추거나 '버벅이는' 현상이 발생합니다.
리액티브 프로그래밍은 UI 이벤트를 스트림으로 처리하고, 백그라운드에서 비동기적으로 작업을 수행한 후 결과를 UI 스레드로 다시 전달하는 방식으로 UI의 반응성을 유지하는 데 탁월합니다.
콜백 지옥(Callback Hell) 및 복잡한 비동기 코드:

전통적인 비동기 처리 방식(특히 콜백 기반)은 비동기 작업이 중첩될수록 코드의 가독성이 떨어지고 유지보수가 극도로 어려워지는 '콜백 지옥' 현상을 초래했습니다.
오류 처리도 파편화되기 쉬웠습니다.
리액티브 프로그래밍은 스트림과 연산자를 통해 비동기 작업을 선언적이고 체인 방식으로 표현하여 이러한 복잡성을 해소하고 코드의 가독성과 관리 용이성을 대폭 개선합니다.

데이터 중심의 프로그래밍 패러다임 변화:

데이터가 지속적으로 생성되고 변화하는 환경(예: 센서 데이터, 실시간 주식 데이터)에서, 변수나 객체가 특정 시점의 상태를 나타내는 것보다, 데이터의 '흐름' 자체를 관리하는 것이 더 효율적인 경우가 많아졌습니다.
리액티브 프로그래밍은 이러한 데이터 흐름을 추상화하여 효과적으로 다룰 수 있도록 돕습니다.

Project Reactor?

Project Reactor는 자바(JVM) 기반의 비동기 및 논블로킹(non-blocking) 애플리케이션을 구축하기 위한 리액티브 프로그래밍 라이브러리입니다. 특히 스프링(Spring) 프레임워크의 리액티브 스택(Spring WebFlux 등)의 핵심 기반으로 사용됩니다.

Application.yml

example

  spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/orders/**
          filters:
            - RewritePath=/orders/(?<path>.*), /$\{path}
            - AddRequestHeader=X-User-Id, #{principal.userId} # ← inject from JWT claims

predicates: defines the conditions that must be met for a specific route to be matched and used. The /** is a wildcard that matches any characters (including slashes) zero or more times.
Therefore, this predicate will match any incoming request where the URI path starts with /orders/.

filters: applied after predicates

  • RewritePath: used to modify the path of the incoming request before it is forwarded to the downstream service. This is particularly useful for exposing a clean API to clients while your backend services might have different internal path structures.

    • In this example, the path "/orders/(?<path>.*)" is replaced by "/${path}".
      • In YAML, if you want to use ${} for variable substitution within a string, you need to escape the $ with a backslash. So, it becomes $\{path}. Your example correctly uses ${path}.
  • AddRequestHeader: used to add a new HTTP header (or overwrite an existing one) to the incoming request before it is forwarded to the downstream service. This is very useful for injecting information like user IDs, tracing IDs, or other contextual data.

    • principal a built-in variable available in SpEL expressions inside filters, thanks to Spring Security.

      • It comes from the Reactor Principal, typically populated by Spring Security when a user is authenticated.

      • If you’re using JWT-based authentication (e.g., with spring-boot-starter-oauth2-resource-server), principal is a JwtAuthenticationToken.

      • If it’s a custom auth mechanism, it could be any object implementing Principal.

Spring Expression Language SpEL

Spring Expression Language (SpEL) is a powerful expression language in the Spring Framework that allows developers to dynamically query, evaluate, and manipulate object graphs at runtime. It’s commonly used in configuration files, annotations like @Value, or within components like Spring Cloud Gateway for dynamic routing, filtering, or bean configuration. SpEL supports features like property access, method invocation, logical operators, and conditional expressions, making it ideal for externalizing logic that would otherwise be hardcoded in Java. This helps improve flexibility, especially in cloud-native or microservice environments where behavior may depend on dynamic data, environment variables, or external conditions.

example (hardcoded)

    String name = person.getProfile().getUser().getName();

example (SpEL)

@Value("#{person.profile.user.name}")
String name;
profile
everything happens for a reason

0개의 댓글