← 목록으로
인프라·클라우드
#서비스 디스커버리#서비스 레지스트리#마이크로서비스#헬스체크#클라이언트/서버 사이드
최종 업데이트 · 2026-10-12

서비스 디스커버리(Service Discovery)와 동적 서비스 위치 탐색

1. 개요

가. 정의

서비스 디스커버리(Service Discovery)란 마이크로서비스·분산 환경에서 수시로 변하는 서비스 인스턴스의 네트워크 위치(IP·포트)를 중앙 레지스트리나 조회 메커니즘을 통해 자동으로 등록·탐색·갱신하여, 호출자가 하드코딩된 주소 없이도 정상 인스턴스로 요청을 보낼 수 있게 하는 메커니즘이다.

서비스 디스커버리는 "누가 어디에 있는가"라는 질문을 런타임에 자동으로 해결하는 장치다. 단일 서버로 운영되던 과거에는 애플리케이션이 데이터베이스나 다른 서비스의 주소를 설정 파일에 고정해 두어도 문제가 없었다. 그러나 수십·수백 개의 서비스가 컨테이너 위에서 생성·소멸을 반복하는 현대 환경에서는 주소가 분 단위로 바뀌므로, 위치를 사람이 관리하는 방식은 더 이상 유효하지 않다. 서비스 디스커버리는 이 변화를 시스템이 스스로 추적하게 하여 호출자와 피호출자의 결합도를 낮춘다.

여기서 중요한 구분은 디스커버리가 단순한 "주소록"이 아니라 상태를 반영하는 동적 주소록이라는 점이다. 레지스트리는 인스턴스의 존재뿐 아니라 그 인스턴스가 지금 요청을 받을 수 있는 상태인지(헬스)를 함께 관리한다. 기술사는 디스커버리를 설명할 때 등록·조회·헬스체크·전파라는 네 가지 축을 함께 다루고, 각 축에서 발생하는 일관성과 가용성의 트레이드오프를 제시해야 한다.

나. 등장 배경과 필요성

전통적인 모놀리식 아키텍처에서는 구성요소가 하나의 프로세스 안에 있으므로 함수 호출로 서로를 찾았다. 3-tier 구조로 분리되더라도 웹·애플리케이션·DB 계층의 주소는 비교적 고정적이어서, 로드밸런서(VIP)와 DNS 레코드를 수동으로 관리하는 것으로 충분했다. 그러나 마이크로서비스로 전환하면 서비스 수가 급격히 늘고, 각 서비스가 독립적으로 배포·확장되며, 오토스케일링과 컨테이너 오케스트레이션이 인스턴스를 수시로 교체한다. 예를 들어 쿠버네티스에서 파드(Pod)가 재시작되면 거의 항상 새 IP를 받으므로, 어제의 주소가 오늘은 전혀 다른 서비스를 가리킬 수 있다.

수동 관리를 고집하면 두 가지 문제가 생긴다. 첫째, 운영자가 주소 변경을 따라가지 못해 설정 드리프트가 누적되고, 죽은 인스턴스로 트래픽이 흘러 장애가 전파된다. 둘째, 배포 때마다 호출자 설정을 함께 바꿔야 하므로 서비스 간 배포 결합이 생겨 독립 배포라는 마이크로서비스의 본질적 이점이 사라진다. 서비스 디스커버리는 위치 정보를 런타임 진실의 원천(Source of Truth)으로 중앙화하고, 변경을 자동 전파하여 이 두 문제를 동시에 해소한다.

실제 사례로, 넷플릭스는 수천 개의 인스턴스가 상시 교체되는 환경에서 자체 레지스트리인 Eureka를 도입해 클라이언트가 가용 인스턴스 목록을 주기적으로 받아 로컬에서 선택하도록 설계했다. 이는 중앙 조회 지점의 장애가 전체 호출을 막지 않도록 가용성을 우선한 선택으로, 디스커버리 설계가 곧 장애 격리 전략과 직결됨을 보여 준다.

다. 핵심 특성

첫째, 동적성이다. 인스턴스의 생성·소멸·주소 변경이 레지스트리에 실시간에 가깝게 반영되어야 한다. 둘째, 헬스 인식이다. 단순히 등록된 인스턴스가 아니라 현재 요청을 처리할 수 있는 인스턴스만 조회 결과에 포함되어야 한다. 셋째, 위치 투명성이다. 호출자는 물리 주소가 아니라 논리적 서비스 이름으로 상대를 지칭하며, 실제 주소 결정은 디스커버리 계층에 위임한다. 넷째, 확장성과 장애 격리다. 레지스트리 자체가 병목이나 단일 장애점이 되지 않도록 복제·캐싱·축퇴(graceful degradation)를 갖춰야 한다.

2. 전체 구조와 동작 모델

서비스 디스커버리는 서비스 공급자(Provider), 서비스 레지스트리(Registry), 서비스 소비자(Consumer) 세 역할과 이들을 잇는 등록·조회·헬스체크 흐름으로 구성된다. 공급자는 기동 시 자신의 위치와 메타데이터를 레지스트리에 등록하고, 소비자는 레지스트리에 질의하여 호출 대상을 얻는다. 레지스트리는 등록된 인스턴스의 생존을 지속적으로 확인하고, 응답하지 않는 인스턴스를 목록에서 제거한다.

graph TD
  subgraph Providers["서비스 공급자 인스턴스"]
    P1["주문서비스 #1 (10.0.1.5:8080)"]
    P2["주문서비스 #2 (10.0.1.7:8080)"]
    P3["결제서비스 #1 (10.0.2.3:9090)"]
  end
  REG["서비스 레지스트리<br/>(등록 정보 + 헬스 상태)"]
  subgraph Consumers["서비스 소비자"]
    C1["API 게이트웨이"]
    C2["프론트엔드 BFF"]
  end
  P1 -->|"① 등록/하트비트"| REG
  P2 -->|"① 등록/하트비트"| REG
  P3 -->|"① 등록/하트비트"| REG
  REG -->|"② 헬스체크"| P1
  REG -->|"② 헬스체크"| P2
  C1 -->|"③ 조회(서비스명)"| REG
  C2 -->|"③ 조회(서비스명)"| REG
  C1 -->|"④ 호출(정상 인스턴스)"| P1
  C2 -->|"④ 호출(정상 인스턴스)"| P3

위 구조에서 핵심은 ①~④가 느슨하게 분리되어 각자의 주기로 동작한다는 점이다. 등록과 하트비트는 공급자가 주도하고, 헬스체크는 레지스트리가 주도하며, 조회는 소비자가 필요할 때 수행한다. 이 분리 덕분에 한 축의 지연이 다른 축을 즉시 막지 않지만, 동시에 각 축 간 시차가 생겨 일시적으로 낡은(stale) 정보가 조회될 수 있다. 따라서 소비자 측에는 조회 결과를 맹신하지 않고 호출 실패 시 다른 인스턴스로 재시도하는 안전장치(재시도·서킷 브레이커)가 함께 필요하다.

레지스트리에 저장되는 정보는 단순 IP·포트에 그치지 않는다. 서비스 이름, 버전, 가용 영역(AZ)·리전, 가중치, 프로토콜, 상태(UP/DOWN/STARTING/OUT_OF_SERVICE) 같은 메타데이터가 함께 저장되어, 소비자가 지역 근접성이나 카나리 비율에 따라 인스턴스를 선택할 수 있게 한다. 예컨대 같은 AZ 내 인스턴스를 우선 선택하면 교차 AZ 트래픽 비용과 지연을 줄일 수 있는데, 이런 정책을 구현하려면 레지스트리가 위치 메타데이터를 보유해야 한다.

3. 디스커버리 방식: 클라이언트 사이드 vs 서버 사이드

디스커버리는 "어디에서 인스턴스를 선택하는가"에 따라 클라이언트 사이드와 서버 사이드로 나뉜다. 이 구분은 로드밸런싱 책임의 위치와 직결되므로 두 방식의 차이를 이유와 함께 이해하는 것이 중요하다.

클라이언트 사이드 디스커버리에서는 소비자가 레지스트리로부터 인스턴스 목록 전체를 받아 로컬에서 로드밸런싱 알고리즘(라운드로빈·가중치·최소 연결 등)을 적용해 직접 호출한다. 중간 홉이 없어 지연이 낮고, 소비자가 세밀한 선택 정책을 가질 수 있다는 장점이 있다. 반면 모든 언어·런타임의 클라이언트에 디스커버리 로직을 심어야 하므로 언어 종속성과 클라이언트 라이브러리 버전 관리 부담이 생긴다. 넷플릭스 Eureka + Ribbon 조합이 대표적이며, 소비자가 주기적으로 레지스트리를 폴링해 로컬 캐시를 유지한다.

서버 사이드 디스커버리에서는 소비자가 고정된 라우터(로드밸런서·API 게이트웨이·쿠버네티스 Service)로만 요청을 보내고, 라우터가 레지스트리를 조회해 정상 인스턴스로 전달한다. 소비자는 디스커버리를 전혀 몰라도 되므로 클라이언트가 단순해지고 언어 중립적이라는 장점이 크다. 대신 라우터라는 홉이 추가되어 지연과 가용성 관리 부담이 라우터로 집중된다. 쿠버네티스의 Service(가상 IP) + kube-proxy, AWS ELB가 이 방식의 전형이다.

sequenceDiagram
  participant C as 소비자(클라이언트)
  participant R as 레지스트리
  participant LB as 라우터/로드밸런서
  participant S as 서비스 인스턴스
  Note over C,S: 클라이언트 사이드
  C->>R: 서비스명으로 인스턴스 목록 조회
  R-->>C: 정상 인스턴스 목록 반환
  C->>C: 로컬 로드밸런싱으로 대상 선택
  C->>S: 선택한 인스턴스 직접 호출
  Note over C,S: 서버 사이드
  C->>LB: 서비스명/가상IP로 요청
  LB->>R: 인스턴스 조회(또는 캐시)
  R-->>LB: 정상 인스턴스 목록
  LB->>S: 로드밸런싱 후 전달
  S-->>LB: 응답
  LB-->>C: 응답 반환

두 방식의 선택은 조직 상황에 따라 달라진다. 단일 언어·소수 팀이라면 클라이언트 사이드가 홉 감소와 세밀한 제어 측면에서 유리하지만, 다수 언어와 외부 연동이 많은 플랫폼이라면 서버 사이드(또는 사이드카 기반)가 운영 일관성 면에서 낫다. 최근에는 두 방식의 절충으로, 디스커버리와 로드밸런싱을 애플리케이션 밖의 사이드카 프록시(서비스 메시의 데이터 플레인)로 옮겨 클라이언트는 로컬호스트로만 호출하고 프록시가 디스커버리를 수행하는 패턴이 확산되고 있다. 이는 클라이언트 사이드의 성능과 서버 사이드의 언어 중립성을 동시에 취하려는 시도다.

4. 서비스 등록 패턴과 헬스 체크

가. 등록 패턴: 셀프 등록 vs 제3자 등록

등록 방식은 누가 레지스트리에 인스턴스를 기록하는가에 따라 나뉜다. 셀프 등록(Self-registration)은 서비스 인스턴스가 스스로 기동 시 레지스트리에 등록하고 종료 시 해지하며, 주기적 하트비트로 생존을 알린다. 구현이 단순하고 인스턴스가 자신의 상태를 가장 잘 알지만, 디스커버리 로직이 애플리케이션 코드에 침투하고 비정상 종료 시 등록이 남는 문제가 있다.

제3자 등록(Third-party registration)은 별도의 등록 관리자(Registrar)가 오케스트레이터의 배포 이벤트를 관찰해 인스턴스를 대신 등록·해지한다. 애플리케이션 코드에서 디스커버리 관심사를 분리할 수 있다는 장점이 크며, 쿠버네티스가 이 방식의 대표다. 쿠버네티스에서는 개발자가 디스커버리 코드를 작성하지 않아도, 컨트롤 플레인이 파드의 라이프사이클을 관찰해 Endpoints/EndpointSlice를 자동으로 갱신한다.

나. 헬스 체크의 설계

헬스 체크는 디스커버리의 신뢰성을 좌우하는 핵심이다. 가장 단순한 방식은 하트비트(heartbeat)로, 인스턴스가 일정 주기로 레지스트리에 신호를 보내고 일정 시간 신호가 없으면 레지스트리가 해당 인스턴스를 만료시킨다. 이 방식은 네트워크 분할 시 멀쩡한 인스턴스가 잘못 만료되는 위험이 있어, 넷플릭스 Eureka는 일정 비율 이상의 하트비트가 끊기면 등록 해지를 보류하는 자기 보호(self-preservation) 모드를 두어 네트워크 문제를 인스턴스 장애로 오인하지 않게 한다.

더 능동적인 방식은 레지스트리나 라우터가 인스턴스의 헬스 엔드포인트(예: /health)를 주기적으로 호출해 응답을 확인하는 것이다. 여기서 라이브니스(liveness)와 레디니스(readiness)를 구분하는 것이 중요하다. 라이브니스는 프로세스가 살아 있는지를, 레디니스는 지금 트래픽을 받을 준비가 되었는지를 뜻한다. 기동 직후 캐시 워밍이나 커넥션 풀 초기화가 끝나지 않은 인스턴스는 살아 있어도 레디니스가 거짓이어야 하며, 이때 디스커버리는 해당 인스턴스를 조회 결과에서 제외해 초기 오류를 막는다. 쿠버네티스는 livenessProbe·readinessProbe·startupProbe를 분리해 이 의미 차이를 명시적으로 다룬다.

5. 주요 구현 기술 비교

서비스 디스커버리 솔루션은 내부적으로 분산 합의나 복제 메커니즘을 사용하며, 그 선택이 CAP 상의 성격(일관성 우선 CP vs 가용성 우선 AP)을 결정한다. 아래 표는 대표 기술을 비교하되, 수치보다 설계 철학의 차이에 주목해야 한다.

기술 합의/복제 CAP 성격 조회 인터페이스 특징
Netflix Eureka 피어 간 비동기 복제 AP(가용성 우선) REST(클라이언트 캐시) 자기 보호 모드, 클라이언트 사이드 전제
HashiCorp Consul Raft CP(기본) DNS + HTTP 멀티 DC, 풍부한 헬스체크, KV 저장소
etcd Raft CP gRPC/HTTP(watch) 쿠버네티스 백엔드, 강한 일관성
Apache ZooKeeper ZAB CP 네이티브 클라이언트(watch) 성숙하나 운영 복잡, 세션 기반
Kubernetes(CoreDNS+Service) etcd 기반 CP(저장) + 캐시 DNS + 가상 IP 플랫폼 내장, 제3자 등록 자동화

CP 계열(Consul·etcd·ZooKeeper)은 네트워크 분할 시 일관성을 지키기 위해 소수 파티션에서 쓰기나 조회를 거부할 수 있다. 반대로 AP 계열(Eureka)은 분할 중에도 각 노드가 가진 (낡을 수 있는) 목록으로 조회에 응답해 가용성을 유지한다. 디스커버리의 본질은 "조금 낡은 목록으로라도 호출을 계속할 수 있는 것"이 "정확하지만 조회가 멈추는 것"보다 나은 경우가 많다는 점이며, 이 때문에 디스커버리 레지스트리에는 AP 성향이 적합하다는 견해가 넓게 받아들여진다. 다만 쿠버네티스처럼 강한 일관성의 etcd를 저장소로 쓰더라도, 실제 조회 경로(CoreDNS·kube-proxy 캐시)는 캐싱과 비동기 반영으로 가용성을 확보하므로, 저장소의 CAP 성격과 데이터 플레인의 체감 성격은 구분해서 이해해야 한다.

예를 들어 멀티 데이터센터를 운영하는 금융 서비스가 Consul을 택하면, DC별 서버 집합이 Raft로 일관성을 유지하면서 WAN 게이트웨이로 DC 간 질의를 중계해, 지역 장애 격리와 교차 DC 조회를 함께 얻을 수 있다. 반대로 하나의 거대한 글로벌 서비스 풀을 운영한다면 Eureka 계열의 가용성 우선 설계가 운영 안정성 면에서 유리할 수 있다.

6. 적용 사례와 설계 예시

첫째, 쿠버네티스 네이티브 디스커버리다. 파드가 생성되면 kubelet이 레디니스 프로브를 통과한 파드의 IP를 Endpoints/EndpointSlice에 반영하고, Service는 안정적인 ClusterIP(가상 IP)와 svc.namespace.svc.cluster.local DNS 이름을 제공한다. 소비자는 http://order-service처럼 서비스 이름만 호출하면 CoreDNS가 이름을 해석하고 kube-proxy가 실제 파드로 분배한다. 개발자가 디스커버리 코드를 전혀 작성하지 않아도 되는 제3자 등록의 전형이다.

둘째, 스프링 클라우드 기반 클라이언트 사이드 디스커버리다. 서비스가 Eureka Client로 자신을 등록하고, 소비자는 Eureka로부터 받은 목록을 Spring Cloud LoadBalancer로 선택해 직접 호출한다. 사내 다수 서비스가 동일 JVM 스택일 때 홉을 줄이고 세밀한 선택 정책(지역 선호·가중치)을 구현하기 좋다.

셋째, 서비스 메시 연계 설계다. Istio·Linkerd 같은 메시는 플랫폼의 디스커버리(쿠버네티스 Endpoints)를 읽어 컨트롤 플레인이 각 사이드카(Envoy)에 엔드포인트를 배포한다. 애플리케이션은 로컬호스트의 프록시로만 호출하고, 프록시가 디스커버리·로드밸런싱·mTLS·재시도를 담당한다. 이렇게 하면 수백 개의 다언어 서비스가 공통 디스커버리·보안 정책을 코드 변경 없이 적용받는다.

7. 심화: 최신 동향과 확장 설계

최근 디스커버리는 세 방향으로 진화하고 있다. 첫째, 디스커버리의 사이드카 이관이다. 서비스 메시의 확산으로 디스커버리·로드밸런싱·장애 처리가 애플리케이션 밖의 데이터 플레인으로 옮겨가, 언어 중립성과 정책 일관성을 동시에 달성하는 흐름이 뚜렷하다. 둘째, 멀티 클러스터·멀티 클라우드 디스커버리다. 단일 클러스터를 넘어 여러 쿠버네티스 클러스터의 서비스를 묶는 표준화 노력(예: 멀티 클러스터 Service API)과 글로벌 로드밸런싱이 결합되어, 지역 장애 시 다른 클러스터로의 페일오버를 디스커버리 계층에서 처리한다. 셋째, 헬스 신호의 정교화다. 단순 생사 판단을 넘어, 응답 지연·에러율 같은 품질 신호를 반영해 degraded(성능 저하) 상태를 구분하고, 가중치를 동적으로 낮추는 이상치 탐지(outlier detection)와 결합한다.

확장 설계 관점에서, 레지스트리는 조회 QPS가 서비스·소비자 수의 곱으로 증가하므로 캐싱이 필수다. 소비자 측 로컬 캐시(일정 TTL)와 서버 측 응답 캐시를 결합하고, 변경을 폴링 대신 watch/스트리밍으로 전파하면 조회 부하와 반영 지연을 동시에 낮출 수 있다. 또한 대규모 환경에서는 전체 엔드포인트를 모든 프록시에 배포하면 메모리·연산이 폭증하므로, 소비자가 실제로 호출하는 서비스의 엔드포인트만 선택적으로 내려주는 범위 한정(scoping)이 확장성의 관건이 된다.

8. 고려사항 및 시사점

첫째, 일관성과 가용성의 명시적 선택이다. 디스커버리 데이터는 다소 낡아도 가용성을 지키는 편이 유리한 경우가 많으므로, 레지스트리의 CAP 성격과 캐시 TTL을 서비스의 장애 허용 수준에 맞춰 설계해야 한다. 지나치게 긴 TTL은 죽은 인스턴스로의 호출을, 지나치게 짧은 TTL은 레지스트리 과부하를 부른다.

둘째, 헬스 체크의 의미 분리와 안전장치다. 라이브니스·레디니스·스타트업을 구분하고, 네트워크 분할을 인스턴스 장애로 오인하지 않도록 자기 보호·오탐 완충을 둔다. 디스커버리만 믿지 말고 소비자 측 재시도·타임아웃·서킷 브레이커를 함께 설계해 낡은 목록으로 인한 실패를 흡수해야 한다.

셋째, 보안과 신뢰 경계다. 레지스트리는 전체 서비스 토폴로지를 담은 민감 자산이므로, 등록·조회에 대한 인증·인가(ACL)와 전송 암호화(mTLS)가 필요하다. 악의적 인스턴스가 임의로 등록해 트래픽을 가로채는 위협(디스커버리 스푸핑)을 막기 위해 등록 주체의 신원 검증이 핵심이며, 제로 트러스트 관점에서 서비스 아이덴티티 기반 인가로 확장하는 추세다.

넷째, 운영 관측성과 벤더 종속이다. 등록 지연·조회 실패·만료율·목록 신선도를 지표로 수집해 디스커버리 자체의 건강을 추적해야 한다. 또한 특정 레지스트리 제품에 디스커버리 로직이 깊게 결합되면 전환 비용이 커지므로, 애플리케이션은 디스커버리 추상화 계층(또는 플랫폼·메시 위임)을 통해 구현 종속을 낮추는 것이 장기적으로 유리하다.

다섯째, 점진적 도입 전략이다. 모놀리식에서 마이크로서비스로 전환할 때 처음부터 완전한 메시를 도입하기보다, 플랫폼 내장 디스커버리(쿠버네티스 DNS)로 시작해 트래픽 제어·보안 요구가 커질 때 사이드카·메시로 단계적으로 확장하는 접근이 복잡도와 효용의 균형 면에서 바람직하다.

참고자료


한 줄 요약: 서비스 디스커버리는 수시로 변하는 인스턴스의 위치를 레지스트리로 중앙화하고 헬스 상태와 함께 자동 전파해, 호출자가 논리 이름만으로 정상 인스턴스에 도달하게 하는 동적 위치 탐색 메커니즘이다.