Software Architecture/Distributed Systems

Distributed Systems - 11. 서비스 디스커버리·게이트웨이

서비스 디스커버리와 API 게이트웨이 — 클라이언트가 어디로 보낼까

2016년 한 회사가 30개의 마이크로서비스를 운영했다. 각 서비스의 URL이 설정 파일에 적혀 있었다. 어느 날 인프라팀이 로드 밸런서를 교체하면서 모든 서비스의 IP가 바뀌었다. 설정 파일을 모두 수정하고 재배포하는 데 하루가 걸렸다. 그 사이 프로덕션은 중단됐다. 서비스가 어디 있는지 알아내는 것조차 복잡한 작업이 된 것이다. 또 다른 문제 — 30개 서비스마다 각자 인증, 속도 제한, 로깅을 구현했는데, 코드가 중복되고 정책이 어긋났다. 이 두 문제를 다루는 패턴이 서비스 디스커버리API 게이트웨이다. 이 글은 두 패턴과 그 현대적 변형인 서비스 메시까지 다룬다.

비유로 감 잡기 — 호텔 프론트 데스크

큰 호텔에 왔다고 상상하자. 200개 객실, 5개의 레스토랑, 수영장, 스파, 비즈니스 센터. 손님이 일일이 "309호 어디 있죠", "레스토랑 영업 시간이 어떻게 되죠"라고 부대시설을 직접 찾아다니면 비효율적이다. 호텔은 프론트 데스크를 둔다. 손님은 프론트에만 문의하고, 프론트가 안내한다.

두 역할:

  • 안내(development) — 손님이 "레스토랑 어디?"라고 물으면 프론트가 위치를 알려준다. 손님이 직접 찾을 필요 없음. 서비스 디스커버리가 이 역할.
  • 중앙 통제(routing) — 손님이 모든 요청(룸서비스 예약, 레스토랑 예약, 체크아웃)을 프론트에만 한다. 프론트가 각 부서로 전달하고, 공통 정책(결제, 인증)을 적용한다. API 게이트웨이가 이 역할.

이 두 가지를 합친 "프론트 데스크"가 — 클라이언트와 여러 서비스 사이에 있어서, 클라이언트의 복잡도를 줄이고 운영 정책을 중앙화한다.

비유의 한계 — 호텔 비유는 '고정된 부대시설'을 가정하지만, 마이크로서비스는 동적으로 생성·소멸한다. 프론트 직원은 매일 객실 위치를 새로 외워야 하는 셈. 또한 호텔은 사람이 안내하지만, 시스템은 자동화된 레지스트리로 동작. 비유는 '동적 변화'에서 무너진다.

서비스 디스커버리 — 서비스 위치를 찾는 메커니즘

마이크로서비스 환경에선 — 서비스가 동적으로 생성·소멸한다. 오토스케일링, 장애 복구, 롤링 배포. IP가 계속 바뀐다. 클라이언트가 IP를 하드코딩하면 첫 번째 변화에 깨진다. 서비스 디스커버리는 — "현재 이 서비스의 인스턴스들이 어디 있는가"를 동적으로 알려주는 메커니즘.

flowchart TD
    subgraph Registry["서비스 레지스트리"]
        R[(etcd / Consul / Eureka)]
    end
    S1[Service A 인스턴스 1] -.->|등록| R
    S2[Service A 인스턴스 2] -.->|등록| R
    S3[Service A 인스턴스 3] -.->|등록| R
    C[Client] -->|조회| R
    R -->|인스턴스 목록 반환| C
    C -.->|로드 밸런스| S1
    C -.->|로드 밸런스| S2

서비스 인스턴스는 시작 시 레지스트리에 자기 위치(IP, 포트)를 등록하고, 종료 시 해제한다. 클라이언트는 레지스트리에서 인스턴스 목록을 받아, 그 중 하나에 요청을 보낸다. 등록은 주기적 하트비트로 갱신돼, 죽은 인스턴스는 자동으로 제거된다.

두 형태의 디스커버리

  • 클라이언트사이드 (Client-side) — 클라이언트가 레지스트리에서 직접 조회하고 인스턴스를 고른다. Netflix Eureka 패턴. 클라이언트가 라이브러리를 포함해야 하지만, 중간 단계가 없어 빠르다.
  • 서버사이드 (Server-side) — 클라이언트는 로드 밸런서에 요청을 보내고, 로드 밸런서가 레지스트리를 조회해 인스턴스를 고른다. AWS ALB, Kubernetes Service 패턴. 클라이언트는 라이브러리 없이 단일 가상 IP로 보낸다.
// 클라이언트사이드 디스커버리
class ClientSideDiscovery {
    ServiceRegistry registry;
    LoadBalancer balancer;

    public Response call(String service, Request req) {
        List<Instance> instances = registry.lookup(service);  // 직접 조회
        if (instances.isEmpty()) throw new NoInstanceException();
        Instance selected = balancer.choose(instances);  // 클라이언트가 선택
        return httpClient.send(selected.address(), req);
    }
}

// 서버사이드 디스커버리 — 클라이언트는 그냥 가상 이름으로 보냄
class ServerSideClient {
    public Response call(Request req) {
        // "order-service"라는 이름으로 보냄 — 실제 인스턴스는 로드 밸런서가 결정
        return httpClient.send("http://order-service/orders", req);
    }
}

Kubernetes의 Service 추상화가 서버사이드의 대표적 예다. 클라이언트는 order-service라는 이름으로 보내고, kube-proxy가 그 이름을 현재 살아 있는 Pod 중 하나로 라우팅한다.

API 게이트웨이 — 공통 관심사의 중앙화

마이크로서비스에선 — 각 서비스가 인증, 속도 제한, 로깅, 메트릭을 각자 구현하면 코드 중복이 심하다. 또한 — 클라이언트가 여러 서비스에 직접 닿으면, 클라이언트 코드가 복잡해진다 (서비스 경계를 다 알아야). API 게이트웨이는 — 클라이언트와 서비스 사이에 단일 진입점을 두고, 공통 관심사를 한 곳에서 처리한다.

flowchart LR
    C1[Client 1] --> GW[API Gateway]
    C2[Client 2] --> GW
    C3[Client 3] --> GW
    GW -->|인증 후| S1[Order Service]
    GW -->|인증 후| S2[Payment Service]
    GW -->|인증 후| S3[Inventory Service]
    GW -. 속도 제한/로깅 .- GW

API 게이트웨이가 담당하는 전형적 역할:

역할 설명
인증 클라이언트 자격 증명 검증 — 서비스들은 신뢰하는 호출만 받음
라우팅 요청을 적절한 서비스로 전달
속도 제한 (rate limiting) 클라이언트별 요청 수 제한 — 서비스 보호
부하 분산 (load balancing) 같은 서비스의 여러 인스턴스에 분산
로깅·메트릭 모든 요청의 메타데이터 수집 — 관측성
응답 집계 (aggregation) 여러 서비스 호출 결과를 하나로 묶어 클라이언트에 반환 — 클라이언트 호출 수 감소
프로토콜 변환 외부 REST를 내부 gRPC로 변환
캐싱 자주 오는 요청의 응답 캐싱

응답 집계가 특히 유용한 예 — 모바일 클라이언트가 "주문 페이지"를 띄우려면 주문, 결제, 배송 정보가 다 필요하다. 게이트웨이 없으면 — 클라이언트가 3번 호출. 게이트웨이가 집계하면 — 클라이언트는 게이트웨이에 1번만, 게이트웨이가 내부적으로 3번 호출 후 합쳐서 반환. 모바일 네트워크 환경에서 특히 큰 절약 (BFF, Backend for Frontend 패턴).

게이트웨이 vs 디스커버리 — 다른 관심사

두 패턴은 함께 쓰이지만 관심사가 다르다.

  • 서비스 디스커버리 — "서비스가 어디 있는가"를 다룸. 인프라 수준의 조회 서비스.
  • API 게이트웨이 — "어떻게 호출할까"를 다룸. 애플리케이션 수준의 중앙 통제.

게이트웨이는 보통 디스커버리를 사용한다 — 게이트웨이가 라우팅하려면 인스턴스 목록이 필요하고, 그건 디스커버리에서 받는다. 두 패턴이 상호 보완적.

설계 사례 — 게이트웨이 필터 체인

Spring Cloud Gateway, Kong, Envoy 같은 게이트웨이는 "필터 체인" 패턴으로 구성된다. 각 공통 관심사를 필터로 구현하고, 요청이 필터를 차례로 통과하게 한다.

// 게이트웨이 필터 체인 (개념적)
class Gateway {
    List<Filter> preFilters = List.of(
        new RateLimitFilter(100, Duration.ofMinutes(1)),  // 분당 100 요청
        new AuthenticationFilter(),
        new LoggingFilter(),
        new TracingFilter()  // 분산 추적 헤더 추가
    );
    List<Filter> postFilters = List.of(
        new MetricsFilter(),
        new CachingFilter(Duration.ofMinutes(5))
    );
    Router router;

    public Response handle(Request req) {
        try {
            // 1. Pre 필터 — 순차 적용
            for (Filter f : preFilters) {
                req = f.apply(req);
                if (req.isShortCircuited()) {
                    return f.fallbackResponse();  // 거부 (rate limit 초과 등)
                }
            }

            // 2. 라우팅 — 디스커버리로 인스턴스 찾아 전달
            Route route = router.match(req.path());
            Instance instance = discovery.lookup(route.service());
            Response resp = httpClient.send(instance.address(), req);

            // 3. Post 필터
            for (Filter f : postFilters) {
                resp = f.apply(resp);
            }
            return resp;
        } catch (Exception e) {
            return fallback(req, e);
        }
    }
}

class RateLimitFilter {
    private final TokenBucket bucket = new TokenBucket(100, Duration.ofMinutes(1));

    public Request apply(Request req) {
        if (!bucket.tryConsume()) {
            req.shortCircuit(429, "Rate limit exceeded");
        }
        return req;
    }
}

class AuthenticationFilter {
    public Request apply(Request req) {
        String token = req.header("Authorization");
        if (token == null) {
            req.shortCircuit(401, "Missing token");
            return req;
        }
        Claims claims = jwt.verify(token);
        req.attach("userId", claims.subject());
        return req;
    }
}

이 구조의 장점 — 각 필터가 단일 관심사만 담당하고, 순서를 자유롭게 조정할 수 있다. 새 정책(예: IP 기반 차단)을 추가하려면 필터 하나만 추가하면 된다.

게이트웨이 안티패턴 — "지능적 게이트웨이"

게이트웨이의 일반적 함정 — 비즈니스 로직을 게이트웨이에 넣는 것. 게이트웨이는 공통 관심사(인증, 속도 제한, 라우팅)만 담당해야지, "이 고객은 VIP니까 할인 적용" 같은 비즈니스 로직을 담으면 안 된다. 그렇게 하면 게이트웨이가 또 하나의 모놀리스가 된다.

// 안티패턴 — 게이트웨이에 비즈니스 로직
class BadGateway {
    Response handle(Request req) {
        if (req.path().equals("/orders") && req.method() == POST) {
            OrderRequest order = parse(req);
            // 게이트웨이에서 비즈니스 규칙!
            if (order.total().compareTo(Money.won(1_000_000)) > 0) {
                if (!isVipCustomer(order.customerId())) {
                    return Response.forbidden();  // 비VIP 큰 금액 주문 금지
                }
            }
            // 게이트웨이가 주문 서비스 흉내
            // ...
        }
    }
}

이 코드는 — 게이트웨이가 주문 서비스의 비즈니스 규칙을 알아야 한다. 규칙이 바뀌면 게이트웨이도 바꿔야 한다. 게이트웨이는 라우팅만, 비즈니스는 서비스에 두는 게 원칙.

서비스 메시 — 게이트웨이의 분산형 진화

최근 등장한 서비스 메시(service mesh) — Istio, Linkerd, Consul Connect — 는 게이트웨이 패턴의 분산형 변형이다. 게이트웨이가 한 곳에 있던 공통 관심사를 — 각 서비스 옆에 사이드카(sidecar) 프록시로 분산시킨다.

flowchart TD
    subgraph Pod1["Pod 1"]
        A1[Service A] <--> P1[Envoy 사이드카]
    end
    subgraph Pod2["Pod 2"]
        A2[Service B] <--> P2[Envoy 사이드카]
    end
    P1 <-->|mTLS, retry, circuit breaker| P2

서비스 메시의 장점 — 게이트웨이에 집중된 책임을 분산. 각 서비스 코드는 비즈니스 로직에만 집중하고, 모든 인프라(인증, 암호화, 회복탄력성)는 사이드카가 담당. 언어 독립적 — Java든 Go든 Python이든 같은 사이드카 설정.

단점 — 복잡성 증가. 사이드카 설정, 디버깅, 성능 오버헤드. 그래서 — 단순한 시스템엔 오버엔지니어링일 수 있다. Kubernetes 위에서 큰 규모로 운영할 때 진가.

게이트웨이·디스커버리 위반/오용 감지

신호 의미
설정 파일에 IP/포트 하드코딩 동적 디스커버리 부재 — 노드 변경 시 깨짐
모든 서비스가 인증 로직을 중복 구현 게이트웨이 부재 — 코드 중복, 정책 어긋남
게이트웨이에 비즈니스 로직 게이트웨이 모놀리스화 — 결합도 증가
게이트웨이가 단일 장애점 이중화 없음 — 게이트웨이 죽으면 전체 멈춤
게이트웨이 장애 시 폴백 없음 사용자가 빈 응답 받음
서비스 메시를 단순 시스템에 적용 오버엔지니어링 — 복잡성 비용만
게이트웨이 없이 클라이언트가 모든 서비스 직접 호출 클라이언트 복잡, 정책 분산

이번 영역을 마치며

distributed systems의 열두 편을 통해 본 것 — 분산 시스템은 본질적으로 어렵다. 네트워크는 신뢰할 수 없고(01), 일관성·가용성·분단 내성을 다 가질 수 없고(02), 일관성의 스펙트럼이 넓고(03), 여러 노드가 동의하는 게 쉽지 않고(04), 데이터를 나누는 것도(05), 복제하는 것도(06), 트랜잭션을 보장하는 것도(07), 정확히 한 번 처리하는 것도(08), 시간 순서를 정하는 것도(09), 부하를 분산하는 것도(10), 서비스를 발견하고 관리하는 것도(11), 장애를 견디는 것도(12) 각각의 trade-off를 갖는다.

 

이 모든 어려움의 공통점 — 분산이 주는 이점(확장성·가용성·독립성)을 얻기 위해 모놀리스에선 자연스럽던 것을 다시 다뤄야 한다. 단일 시계, 단일 상태, 단일 트랜잭션, 단일 호출. 이걸 포기하고 분산을 택하는 결정이 — 품질 속성(02편 CAP, 03-architectural-styles에서 다룬 트레이드오프 매트릭스)에 따라 정당화될 때만 의미가 있다. 그 정당화 없이 "마이크로서비스가 유행이니까"로 분산을 택하면 — 이 장의 모든 어려움을 만나면서도 그 이점을 못 누리는, 최악의 결과를 낳는다.

 

다음 영역(ddd and patterns)은 — 분산이 아니라 도메인을 다룬다. 시스템을 기술적 분산이 아니라 비즈니스 도메인 경계로 자르는 접근. 분산의 "어떻게 노드를 잇나"(04)와 도메인의 "문제를 어떻게 자르나"(05)는 직교하는 관심사지만, 둘 다 아키텍처의 핵심 축이다. 03-architectural-styles에서 본 마이크로서비스의 "어디서 자를까"라는 질문이 — 05편에서 더 깊이 답해진다.

분산 시스템의 핵심 통찰 — "강한 것"은 항상 비싸다. 강한 일관성(linearizability)은 느리고, 강한 가용성(always-on)은 일관성을 희생하고, 강한 보장(exactly-once)은 복잡하다. 이 비용을 직시하고 — 어디까지 강하게 하고 어디서 약하게 타협할지 결정하는 게 분산 시스템 설계의 본질이다. "전부 강하게"는 없다. trade-off 없는 분산 시스템은 없다.


참고

  • Newman — (2nd ed, O'Reilly, 2021), Ch.5 (서비스 디스커버리)·Ch.8 (배포·게이트웨이) — 접근 2026-07-20
  • Richards, Ford — (O'Reilly, 2020), 서비스 디스커버리·API 게이트웨이 장
  • Burns et al. — "Borg, Omega, and Kubernetes" (CACM, 2016), 서비스 추상화의 배경 — 접근 2026-07-20
  • Kubernetes Documentation — "Service" 리소스, kube-proxy의 라우팅 — 접근 2026-07-20
  • Istio Documentation — 서비스 메시 아키텍처, 사이드카 모델 — 접근 2026-07-20
  • Fowler — "BFF — Backend for Frontend" (martinfowler.com/bliki) — 접근 2026-07-20