무중단 동적 라우팅 구현기

무중단 동적 라우팅 구현기

프로젝트 배경과 Spring Cloud Gateway 선택 이유

본 프로젝트는 급변하는 비즈니스 요구사항에 기민하게 대응하고 서비스 간 간섭 없는 개별 확장성(Scalability)을 확보하기 위해 마이크로서비스 아키텍처(MSA)를 전면 채택하였습니다. 특히 도메인별 전문성을 가진 다수의 개발사가 참여하는 '멀티 벤더(Multi-vendor) 협업 구조'로 진행되어 병렬 개발이 이루어졌습니다. 다만, 아키텍처 관점에서는 서비스 경계가 파편화되고 관리 포인트가 급증하는 관리적 복잡성이라는 과제가 뒤따랐습니다.

성공적인 프로젝트 수행을 위해 해결이 필수적이었던 핵심 이슈는 다음과 같습니다.

  • 다중 개발사 환경의 관리 복잡성 해소: 수십 개의 서비스가 독립적으로 배포·롤백되는 과정에서 각 서비스의 엔드포인트를 일일이 동기화하는 것은 불가능합니다. 네트워크 위치를 추상화하고 요청을 연결해 주는 중앙 집중식 라우팅 체계가 필수적이었습니다.

  • 보안 기능 중앙화를 통한 생산성 극대화: 인증(Authentication)과 인가(Authorization)를 서비스마다 개별 구현하는 것은 중복 개발이며 시스템 전체의 보안 취약점을 유발합니다. 이를 Gateway에서 통합 구현하여 전체 서비스의 보안 수준을 상향 동기화해야 했습니다.

  • 폐쇄망 인프라 내 안정적인 외부 연동: 본 시스템은 기밀성 유지를 위해 내부 폐쇄망에 구축되었으나 외부 연동 접점이 반드시 필요했습니다. 내부 서비스를 직접 노출하지 않으면서 통신을 중재하고 내부 토폴로지를 은닉하는 '보안 관문(Secure Gate)'이 핵심이었습니다.

  • 기능 변경 및 추가를 위한 유연성 확보: 급변하는 요구사항과 연동 규격에 대응하기 위해, 인프라 설정에 종속적인 하드웨어 장비 대신 비즈니스 로직을 코드로 제어할 수 있는 유연한 게이트웨이가 필요했습니다.

이러한 요구사항을 종합적으로 충족하기 위해 우리는 Spring Cloud Gateway(SCG)를 선택했습니다. SCG는 JVM 생태계와 완벽히 통합될 뿐만 아니라, Netty 기반의 비동기 논블로킹(Non-blocking) 아키텍처를 통해 적은 리소스로도 대규모 동시 연결을 안정적으로 처리합니다.

무엇보다 Java 코드로 필터 로직을 자유롭게 커스터마이징할 수 있다는 점이 결정적이었습니다. 이를 통해 단순히 트래픽을 전달하는 역할을 넘어, 다중 개발사 간의 복잡한 권한 제어와 폐쇄망 특화 보안 필터링, 동적 라우팅을 애플리케이션 레벨에서 기민하게 구현할 수 있었습니다.

무중단 동적 라우팅 구현하기

초기 구축 단계에서는 Spring Cloud Gateway(이하 SCG)에서 제공하는 가장 표준적인 방식인 정적 라우팅(Static Routing)을 채택했습니다. 아래와 같이 application.yml 설정 파일에 라우팅 규칙을 직접 명시하는 방식입니다.

spring:
   cloud:
       gateway:
           server:
               webflux:
                   routes:
                      - id: shared
                         predicates:
                             - Path=${server.servlet.context-path}/shared/**
                         uri: <http://foo-bar.com/api/shared>
                         filters:
                             - RewritePath=${server.servlet.context-path}/, /api/
                             - AddRequestHeader=xgate-id, gate1234
                         metadata:
                             response-timeout: 1800000 #30분
                             connect-timeout: 3000 #30초

이 방식은 구조가 직관적이고 구현이 빠르다는 장점이 있지만, 다중 개발사가 참여하고 운영 요구사항이 빈번한 환경에서는 치명적인 단점을 드러냈습니다. 단 하나의 경로를 추가하거나 타임아웃 수치를 수정하더라도 반드시 설정 파일을 변경한 뒤 전체 게이트웨이 서비스를 재빌드하고 배포하는 과정을 거쳐야 했기 때문입니다.

특히 서비스 운영자가 즉시 경로를 제어할 수 없고, 매번 개발자에게 수정 요청을 전달한 뒤 배포 일정에 맞춰 반영을 기다려야 하는 과정은 비즈니스의 기민성을 저해하는 커다란 오버헤드가 되었습니다.

이러한 운영상의 제약을 극복하기 위해, 라우팅 메타데이터를 데이터베이스에서 관리하고 이를 게이트웨이 엔진에 실시간으로 동기화하는 '동적 라우팅' 방식으로의 전환을 결정했습니다. 먼저, 기존 YAML 파일에 흩어져 있던 라우팅 정보를 관계형 데이터 모델로 정규화하기 위해 GatewayRoute 도메인을 아래와 같이 설계하였습니다.

public class GatewayRoute {
    private String id;
    private String routeName;
    private String pathPattern;
    private String rewriteFrom;
    private String rewriteTo;
    private String uri;
    private long responseTimeout;
    private long connectTimeout;
    private boolean active;
    private List<ApiPermission> apiPermissions;
    private boolean useQueue;
    private int maxQueueSize;
    private int maxConcurrent;
    private List<KeyValue> requestHeaders;
}

public class ApiPermission {
    private String path;
    private List<String> permittedServices;
}

이 도메인 모델은 정적 방식에서 다루던 모든 라우팅 정보뿐만 아니라, 향후 설명할 [통합 인증 및 인가 필터]와 [대기열 관리]를 위한 메타데이터까지 통합적으로 관리할 수 있도록 확장되었습니다.

또한, 동적으로 저장된 데이터를 실제 라우팅에 적용하기 위한 기술적 핵심은 SCG의 내부 메커니즘을 활용하는 데 있습니다. SCG는 초기화 시점에 RouteDefinitionLocator 인터페이스의 구현체를 자동으로 감지하여 라우트 목록을 로드합니다.

우리는 이를 위해 커스텀 GatewayRouteConfig 클래스를 생성하고, DB에서 라우트 정보를 조회하여 SCG가 이해할 수 있는 RouteDefinition 형태로 변환해 주는 로직을 구현하였습니다. 이를 통해 운영자는 관리자 UI에서 설정값을 변경하는 것만으로, 게이트웨이 재시작 없이 실시간으로 라우팅 규칙을 적용할 수 있는 환경을 완성했습니다.

@Configuration
public class GatewayRouteConfig {

        @Bean
        public RouteDefinitionLocator dbRouteDefinitionLocator() {
        return () -> policySeek.findAllGatewayRoutes()
        .filter(GatewayRoute::isActive)
        .doOnNext(route -> {
                queueManager.updateRouteConfig(
                   route.getId();
                   route.getMaxQueueSize(),
                   route.getMaxConcurrent()
                );
           }
        .map(this::convertToRouteDefinition);
        }

        private RouteDefinition convertToRouteDefinition(GatewayRoute route){
        RouteDefinition definition = new RouteDefinition();
        definition.setId(route.getId());
        definition.setUri(route.getUri());
        String fullPath = normalizedContexPath() + route.getPathPattern();
        definition.setPredicates(List.of(new PredicateDefinition("Path=": + fullPath)));

        List<FilterDefinition> filters = new ArrayList<>();
        if (route.isUseQueue()){
                filters.add(new FilterDefinition("Queue"));
        }
        filters.add(new FilterDefinition("RewritePath=" + normalizeContextPath() + route.getRewriteFrom() + "," + route.getRewriteTo()));
        route.getRequestHeaders().forEach(keyValue -> filters.add((new FilterDefinition("AddRequestHeader=" + keyValue.getKey() + "," + keyValue.getValue())));
        definition.setFilters(filters);
     
        definition.getMetadata().put("response-timeout", route.getResponseTimeout());
        definition.getMetadata().put("connect-timeout", route.getConnectTimeout());
        }
}

마치며

지금까지 Spring Cloud Gateway를 활용해 기존의 정적 YAML 설정 방식에서 벗어나, 데이터베이스 기반의 무중단 동적 라우팅 아키텍처를 구축한 과정을 살펴보았습니다. 인프라 설정을 비즈니스 데이터의 영역으로 끌어올린 이번 고도화 작업은 실무적으로 다음과 같은 확실한 이점을 가져다주었습니다.

  • 운영과 배포의 완전한 분리: 단 하나의 API 경로를 추가하거나 타임아웃 수치를 조정하기 위해 시스템 전체를 재빌드하고 배포하던 비효율이 사라졌습니다. 이제는 운영 중에도 서비스 중단 없이 실시간으로 라우팅 규칙을 적용할 수 있습니다.

  • 멀티 벤더 환경에서의 소통 비용 절감: 여러 개발사의 마이크로서비스가 각기 다른 시점에 배포되거나 엔드포인트를 변경하더라도, 게이트웨이단에서 데이터를 수정하는 것만으로 기민하게 대응할 수 있는 완충 지대가 마련되었습니다.

  • 유연한 아키텍처적 확장성: 라우팅 메타데이터를 정규화된 도메인 모델로 관리하게 되면서, 단순한 트래픽 포워딩을 넘어 보안과 트래픽 제어를 유기적으로 결합할 수 있는 단단한 뼈대를 완성했습니다.

하지만 동적 라우팅 환경을 안정적으로 구축한 것은 거대한 도화지를 펼친 것에 불과합니다. 진짜 실무적인 고민은 이 분산된 인프라 위에서 어떻게 보안 표준을 유지하고, 밀려드는 대규모 트래픽을 안전하게 제어할 것인가로 이어집니다. 다음 편에서는 이번 편에서 완성한 동적 라우팅 인프라를 기반으로 구동되는 핵심 비즈니스 보안 및 제어 아키텍처를 다룰 예정입니다.

Hustle Paul

Site footer