본문 바로가기
Spring/Cloud

[Spring] API Gateway부터 Service Discovery까지: Spring Cloud Gateway 전체 구조와 동작 흐름

by coding_whale 2026. 4. 15.
반응형

서비스를 마이크로서비스로 분리하기 시작하면 기능 개발보다 먼저 구조를 통제할 기준이 필요해진다. 요청이 어떤 경로로 들어오고, 공통 정책이 어디에서 적용되며, 서비스 인스턴스가 바뀌어도 호출이 끊기지 않게 하려면 무엇을 기준으로 설계해야 하는지부터 정리해야 한다. 이번 글은 기준을 API Gateway 중심으로 잡고, Spring Cloud Gateway의 처리 방식과 WebFlux 필터 체인, 그리고 Eureka 기반 디스커버리까지 하나로 연결해 보여준다.

<이전 글>

 

서비스 디스커버리의 시작: Spring Cloud Netflix Eureka

스프링 클라우드 개요와 Spring Cloud Netflix Eureka 정리스프링 클라우드는 MSA 운영에서 반복되는 문제를 공통 컴포넌트로 해결해주는 생태계이고, Eureka는 서비스 주소를 동적으로 등록/ 조회하는 서

myblog01150.tistory.com

 

 

API Gateway를 먼저 두는 이유

API Gateway는 단순히 트래픽을 넘겨주는 통로가 아니라, 외부 요청이 내부 서비스로 흩어지기 전에 정책을 한 번에 정리하는 경계 지점이다. 서비스마다 인증/로깅/헤더 처리를 따로 구현하면 처음에는 빨라 보여도 운영 단계에서 일관성이 무너진다. Gateway를 진입점으로 세우면 라우팅과 공통 정책이 한 곳에 모이고, 서비스가 늘어날수록 이 중앙화의 장점이 더 크게 드러난다.

실제 요청은 게이트웨이에 먼저 도착한 뒤 경로 조건을 기준으로 라우트가 선택되고, 필터 체인을 거쳐 내부 서비스로 전달된다. 응답이 돌아올 때도 같은 체인을 역방향으로 통과하면서 필요한 관측 로그를 남길 수 있어, 장애 분석 시점에 “어디까지 정상적으로 흘렀는지”를 잡아내기 쉬워진다.

 

Spring Cloud Gateway에서 라우팅이 잡히는 방식

Spring Cloud Gateway는 라우트를 고정 URL로 묶기보다 서비스 이름 중심으로 설정하는 데 강점이 있다. 코드에서도 Path 매칭으로 진입 경로를 분기하고, 대상 URI를 lb://서비스명으로 정의해 디스커버리 레이어와 자연스럽게 연결한다. 이 방식은 서비스 인스턴스 주소가 바뀔 때마다 게이트웨이 설정을 뜯어고치는 부담을 줄여준다.

spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: first-service
              # 서비스 이름 기반으로 대상 선택
              uri: lb://MY-FIRST-SERVICE
              predicates:
                # 요청 경로에 따라 first-service 라우트 선택
                - Path=/first-service/**
            - id: second-service
              # 두 번째 서비스도 동일한 방식으로 라우팅
              uri: lb://MY-SECOND-SERVICE
              predicates:
                - Path=/second-service/**

이 설정을 두면 게이트웨이는 “정해진 서버 주소”가 아니라 “찾아야 하는 서비스 이름”을 기준으로 움직인다. 그래서 서비스 확장이나 재시작이 발생해도 라우팅 정책 자체는 비교적 안정적으로 유지된다.

WebMVC와 WebFlux 차이를 Gateway 관점에서 보면

 

WebMVC와 WebFlux를 비교할 때 중요한 건 문법보다 처리 모델이다. WebMVC는 요청당 스레드 모델이라 개발 경험이 익숙하고 디버깅 흐름이 직관적이다. 반면 WebFlux는 논블로킹 방식으로 동작해 동시 요청과 I/O 대기 구간이 많은 환경에서 효율을 노릴 수 있다.

다만 WebFlux가 항상 정답은 아니다. 성능 잠재력은 분명하지만, 리액티브 체인 디버깅과 운영 관측에 대한 팀 숙련도가 부족하면 오히려 장애 대응 시간이 늘어날 수 있다. 결국 선택 기준은 “기술 스택 선호”가 아니라 트래픽 특성, 팀 역량, 운영 방식까지 포함해 판단해야 한다.

 

Filter를 나눠서 설계하는 이유: Custom, Global, Logging

필터를 분리하는 핵심은 책임 경계를 분명히 하는 데 있다. 라우트별로 다르게 처리할 내용과 전 서비스에 공통으로 적용할 정책, 그리고 관측 순서를 보장해야 하는 로깅 포인트를 한 필터에 몰아넣으면 시간이 갈수록 수정 비용이 커진다. 강의 코드처럼 역할을 나눠두면 변경이 필요한 지점을 빠르게 찾고 영향 범위를 제한할 수 있다.

 

CustomFilter: 라우트별 컨텍스트를 빠르게 확인하는 지점

CustomFilter는 특정 라우트에서 요청이 들어온 시점과 응답이 나가는 시점을 가볍게 추적할 때 효과적이다. 공통 필터와 분리되어 있어 라우트 단위 검증이나 임시 추적 포인트를 붙이기 쉽다.

return (exchange, chain) -> {
    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();

    // 라우트 진입 시점 식별
    log.info("Custom PRE Filter: request id -> {}", request.getId());

    return chain.filter(exchange).then(Mono.fromRunnable(() -> {
        // 라우트 응답 시점 식별
        log.info("Custom POST Filter: response code -> {}", response.getStatusCode());
    }));
};

이렇게 pre/post 로그를 묶어두면 특정 경로에서 발생한 지연이나 오류를 라우트 컨텍스트 안에서 더 빠르게 파악할 수 있다.

 

GlobalFilter: 공통 정책을 한 곳에 모으는 지점

GlobalFilter는 모든 라우트에서 반복되는 정책을 전역으로 적용할 때 중심 역할을 한다. default-filters를 통해 한 번 선언하면 전체 라우트에 동일한 기준이 적용되어, 서비스별 편차가 생기는 문제를 줄일 수 있다.

default-filters:
  - name: GlobalFilter
    args:
      # 공통 메시지 및 로깅 스위치
      baseMessage: Spring Cloud Gateway WebFlux Global Filter
      preLogger: true
      postLogger: true
if (config.isPreLogger()) {
    // 공통 요청 시작 로그
    log.info("Global Filter Start: request id -> {}", request.getId());
}

return chain.filter(exchange).then(Mono.fromRunnable(() -> {
    if (config.isPostLogger()) {
        // 공통 응답 종료 로그
        log.info("Global Filter End: response code -> {}", response.getStatusCode());
    }
}));

운영 관점에서는 이런 전역 정책이 있어야 로그 포맷과 관측 시점을 표준화할 수 있고, 배포 후 로깅 강도를 조절하는 것도 훨씬 수월해진다.

 

 

LoggingFilter: 필터 체인의 순서를 고정하는 지점

필터가 여러 개일 때는 내용만큼 실행 순서가 중요하다. 같은 로그를 남겨도 어느 필터가 먼저 실행됐는지가 불명확하면 원인 추적이 흔들린다. OrderedGatewayFilter로 우선순위를 명시하면 체인 시작점에서 관측 기준을 고정할 수 있다.

GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponse response = exchange.getResponse();

    // 체인 초반에서 요청 URI 기록
    log.info("Logging Filter Start: request uri -> {}", request.getURI().toString());

    return chain.filter(exchange).then(Mono.fromRunnable(() -> {
        // 체인 종료 시 응답 코드 기록
        log.info("Logging Filter End: response code -> {}", response.getStatusCode());
    }));
// 최우선 순위로 실행
}, OrderedGatewayFilter.HIGHEST_PRECEDENCE);

실제 운영에서는 이 우선순위 고정이 로그 해석의 기준선이 되고, 장애 시점의 흐름 복원 속도를 크게 높여준다.

 

 

Spring Cloud Gateway와 Eureka가 맞물리는 방식

 

Eureka는 살아 있는 서비스 인스턴스를 등록하고 조회하는 레지스트리 역할을 한다. 게이트웨이가 lb://MY-FIRST-SERVICE처럼 서비스 이름만 알고 있어도 실제 호출 가능한 인스턴스를 찾아 요청을 전달할 수 있는 이유가 여기에 있다. 이 구조 덕분에 인스턴스 증설, 축소, 재시작 같은 변경이 잦은 환경에서도 라우팅 설정 자체를 자주 변경하지 않아도 된다.

eureka:
  client:
    # 현재 애플리케이션 등록
    register-with-eureka: true
    # 레지스트리 조회 활성화
    fetch-registry: true
    service-url:
      # Eureka 서버 주소
      defaultZone: http://localhost:8761/eureka

게이트웨이가 디스커버리를 통해 대상을 선택하게 만들면, 서비스 위치 변화는 인프라 레이어에서 흡수되고, 애플리케이션 레벨의 라우팅 정책은 더 안정적으로 유지된다.

 

 

정리

이번 주제는 Gateway 기능을 외우는 문제가 아니라, 경계와 책임을 어떻게 나눌지에 대한 설계 문제다. API Gateway로 진입점을 통제하고, Spring Cloud Gateway에서 라우트와 필터 책임을 분리하며, Eureka로 서비스 위치 변화를 흡수하면 변화가 많은 MSA 환경에서도 운영 안정성을 유지하기 쉬워진다. 결국 중요한 건 기술 요소를 개별적으로 도입하는 것이 아니라, 요청 흐름 전체를 기준으로 역할을 맞물리게 설계하는 것이다.

반응형