🔍 서비스 디스커버리 패턴

패턴 개요

서비스 디스커버리는 마이크로서비스 환경에서 서비스 인스턴스의 네트워크 위치(IP, Port)를 동적으로 찾아내는 메커니즘입니다. 클라우드 환경에서 자동 스케일링, 장애 복구 시 서비스 위치가 동적으로 변경되는 문제를 해결합니다.

중요도: ⭐⭐⭐ 필수 패턴

2026년 현재 클라우드 네이티브 환경에서는 서비스 디스커버리가 필수입니다. Kubernetes, Service Mesh 등 현대적인 인프라는 서비스 디스커버리를 기본으로 제공합니다.


📑 목차


1. 핵심 개념

🎯 정의

서비스 디스커버리는 **서비스 레지스트리(Service Registry)**를 통해 마이크로서비스 인스턴스들의 위치 정보를 자동으로 관리하고, 클라이언트가 필요한 서비스를 동적으로 찾을 수 있게 하는 패턴입니다.

핵심 특징:

  • 동적 위치 파악: 서비스 IP/Port가 변경되어도 자동 감지
  • 자동 등록/해제: 서비스 시작 시 자동 등록, 종료 시 자동 해제
  • 헬스 체크: 정상 인스턴스만 반환
  • 로드 밸런싱: 여러 인스턴스 중 선택

📊 기본 구조

┌─────────────────────────────────────────────────┐
│          Service Registry (중앙 저장소)          │
│                                                 │
│  ┌──────────────────────────────────────────┐  │
│  │ Service A: 10.0.1.5:8080 (healthy)       │  │
│  │ Service A: 10.0.1.6:8080 (healthy)       │  │
│  │ Service B: 10.0.2.3:8081 (healthy)       │  │
│  │ Service C: 10.0.3.7:8082 (unhealthy)     │  │
│  └──────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘
        ↑ 등록              ↓ 조회
        │                  │
┌───────┴────────┐   ┌─────┴──────┐
│  Service A     │   │ Service B  │
│  (Provider)    │   │ (Consumer) │
└────────────────┘   └────────────┘

동작 흐름:

  1. 등록 (Registration): 서비스 인스턴스가 시작되면 자신의 정보를 레지스트리에 등록
  2. 조회 (Discovery): 클라이언트가 호출할 서비스의 위치를 레지스트리에서 조회
  3. 헬스 체크 (Health Check): 레지스트리가 주기적으로 서비스 상태 확인
  4. 해제 (Deregistration): 서비스 종료 시 레지스트리에서 제거

2. 문제와 해결

🚨 해결하려는 문제

문제 1: 고정 IP의 한계

전통적인 방식 (설정 파일에 하드코딩):

# application.yml
services:
  payment:
    host: 192.168.1.100
    port: 8080

문제점:

  • 서비스 재시작 시 IP가 변경되면 설정 파일 수정 필요
  • Auto Scaling 시 새로운 인스턴스의 IP를 수동으로 추가해야 함
  • 장애 발생 시 수동으로 제거해야 함
  • 배포 시마다 설정 변경으로 인한 다운타임 발생

문제 2: 클라우드 환경의 동적 특성

클라우드 환경의 특징:

09:00 - Service A 인스턴스 3개 (10.0.1.5, 10.0.1.6, 10.0.1.7)
10:00 - Auto Scaling: 5개로 증가 (10.0.1.8, 10.0.1.9 추가)
11:00 - 장애 발생: 10.0.1.6 다운
11:05 - 자동 복구: 10.0.1.10 생성
14:00 - Auto Scaling: 2개로 감소

문제점:

  • 매번 IP 주소가 변경됨
  • 수동 관리 불가능
  • 장애 인스턴스 제거 지연

문제 3: 여러 환경 관리

Development:  payment-service → 192.168.1.100
Staging:      payment-service → 10.20.30.40
Production:   payment-service → 172.31.50.60 (x3 instances)

문제점:

  • 환경별로 다른 설정 파일 유지
  • 설정 불일치로 인한 오류 발생
  • 관리 복잡도 증가

✅ 서비스 디스커버리의 해결 방법

해결 1: 동적 위치 관리

// 서비스 디스커버리 사용
List<ServiceInstance> instances = discoveryClient.getInstances("payment-service");
// 현재 살아있는 모든 인스턴스 자동 반환
// [10.0.1.5:8080, 10.0.1.6:8080, 10.0.1.7:8080]
  • IP 변경되어도 코드 수정 불필요
  • 서비스 이름만으로 접근 가능
  • 자동으로 최신 정보 반환

해결 2: 자동 헬스 체크

Service Registry:
├─ payment-service: 10.0.1.5 (✅ healthy, last check: 2s ago)
├─ payment-service: 10.0.1.6 (❌ unhealthy, last check: 35s ago) → 제외
└─ payment-service: 10.0.1.7 (✅ healthy, last check: 1s ago)
  • 장애 인스턴스 자동 제외
  • 복구된 인스턴스 자동 추가
  • 수동 개입 불필요

해결 3: 환경 독립성

// 모든 환경에서 동일한 코드
String response = restTemplate.getForObject(
    "http://payment-service/api/charge",
    String.class
);
// 각 환경의 Service Registry가 해당 환경의 IP 반환
  • 환경별 설정 불필요
  • 동일한 애플리케이션 코드 사용
  • 배포 단순화

3. 디스커버리 패턴 분류

📐 Client-Side Discovery (클라이언트 측 디스커버리)

정의: 클라이언트가 직접 Service Registry에서 인스턴스 목록을 조회하고 로드 밸런싱을 수행

┌─────────────┐     1. 조회      ┌─────────────────┐
│  Client     │ ───────────────> │ Service Registry│
│  (Service B)│ <─────────────── │ (Eureka/Consul) │
└─────────────┘  2. 인스턴스 목록  └─────────────────┘
       │
       │ 3. 직접 호출 (로드밸런싱)
       ├──────────> Service A (10.0.1.5)
       ├──────────> Service A (10.0.1.6)
       └──────────> Service A (10.0.1.7)

특징:

  • 클라이언트가 모든 제어권 보유
  • 로드 밸런싱 알고리즘 선택 가능
  • 캐싱으로 성능 향상

대표 구현체:

  • Netflix Eureka
  • Netflix Ribbon (Client-side Load Balancer)

장점:

  • 유연한 로드 밸런싱 전략
  • Registry 장애 시에도 캐시된 정보로 동작 가능

단점:

  • 클라이언트 복잡도 증가
  • 각 언어/플랫폼마다 클라이언트 라이브러리 필요

📐 Server-Side Discovery (서버 측 디스커버리)

정의: 클라이언트는 로드 밸런서에 요청하고, 로드 밸런서가 Service Registry를 조회

┌─────────────┐     1. 요청      ┌──────────────────┐
│  Client     │ ───────────────> │  Load Balancer   │
│  (Service B)│                  │  (Kubernetes Svc)│
└─────────────┘                  └──────────────────┘
                                         │ 2. 조회
                                         ↓
                                 ┌─────────────────┐
                                 │Service Registry │
                                 │   (etcd/DNS)    │
                                 └─────────────────┘
                                         │
                       3. 인스턴스 선택 & 전달
                       ├──────────> Service A (10.0.1.5)
                       ├──────────> Service A (10.0.1.6)
                       └──────────> Service A (10.0.1.7)

특징:

  • 클라이언트는 서비스 이름만 알면 됨
  • 인프라 레벨에서 처리
  • 플랫폼 독립적

대표 구현체:

  • Kubernetes Service
  • AWS ELB + Route 53
  • HashiCorp Consul (Server mode)

장점:

  • 클라이언트 단순화
  • 언어/플랫폼 독립적
  • 중앙화된 관리

단점:

  • 로드 밸런서가 SPOF가 될 수 있음
  • 추가 네트워크 홉 발생
  • 커스터마이징 제한적

📊 패턴 비교

구분Client-SideServer-Side
제어 위치클라이언트인프라
로드밸런싱클라이언트가 선택로드밸런서가 선택
복잡도클라이언트 복잡클라이언트 단순
네트워크 홉1 hop2 hops
플랫폼 의존성라이브러리 필요독립적
대표 예시Netflix EurekaKubernetes

4. 주요 구현체

1. Netflix Eureka (Client-Side)

Spring Cloud 생태계 표준

Java/Spring 환경에서 가장 널리 사용되는 서비스 디스커버리

아키텍처:

┌──────────────────────────────────────────────┐
│           Eureka Server (Registry)           │
│                                              │
│  Service Registry DB                         │
│  ├─ order-service: 10.0.1.5:8080            │
│  ├─ user-service: 10.0.2.3:8081             │
│  └─ payment-service: 10.0.3.7:8082          │
└──────────────────┬───────────────────────────┘
                   ↕ Heartbeat (30초마다)
┌──────────────────────────────────────────────┐
│           Eureka Clients                     │
│                                              │
│  ┌──────────────┐  ┌──────────────┐         │
│  │Order Service │  │ User Service │         │
│  │              │  │              │         │
│  │ + Ribbon     │  │ + Ribbon     │         │
│  │ (Load        │  │ (Load        │         │
│  │  Balancer)   │  │  Balancer)   │         │
│  └──────────────┘  └──────────────┘         │
└──────────────────────────────────────────────┘

주요 기능:

  • 자가 등록: 서비스 시작 시 자동 등록
  • Heartbeat: 30초마다 살아있음을 전송
  • 클라이언트 캐싱: 로컬 캐시로 Registry 장애 대비
  • Ribbon 통합: 클라이언트 사이드 로드 밸런싱

장점:

  • Spring Boot 자동 설정
  • Netflix OSS 생태계와 완벽 통합
  • 풍부한 커뮤니티

단점:

  • AP (Availability & Partition tolerance) 우선 (일관성 약함)
  • Java 외 언어 지원 제한적

2. HashiCorp Consul

멀티 데이터센터 지원

서비스 디스커버리 + KV 스토어 + 헬스 체크 올인원

아키텍처:

┌─────────────────────────────────────────────┐
│         Consul Cluster (Raft)               │
│                                             │
│  Server 1  ←→  Server 2  ←→  Server 3      │
│  (Leader)      (Follower)    (Follower)    │
└─────────────────┬───────────────────────────┘
                  ↕ HTTP/DNS API
┌─────────────────────────────────────────────┐
│            Consul Agents                    │
│                                             │
│  ┌──────────────┐  ┌──────────────┐        │
│  │  Service A   │  │  Service B   │        │
│  │              │  │              │        │
│  │ + Agent      │  │ + Agent      │        │
│  └──────────────┘  └──────────────┘        │
└─────────────────────────────────────────────┘

주요 기능:

  • DNS 인터페이스: payment.service.consul로 조회
  • HTTP API: RESTful API 제공
  • 헬스 체크: Script, HTTP, TCP, TTL 체크 지원
  • KV 스토어: 설정 관리
  • Multi-DC: 여러 데이터센터 간 동기화

헬스 체크 예시:

{
  "service": {
    "name": "payment-service",
    "port": 8080,
    "checks": [
      {
        "http": "http://localhost:8080/health",
        "interval": "10s",
        "timeout": "1s"
      }
    ]
  }
}

장점:

  • 강력한 일관성 (Raft 알고리즘)
  • 언어 독립적 (HTTP/DNS)
  • 멀티 데이터센터 기본 지원

단점:

  • 설정 복잡도
  • 운영 오버헤드

3. Kubernetes Service

클라우드 네이티브 표준

2026년 현재 가장 많이 사용되는 Server-Side Discovery

아키텍처:

┌─────────────────────────────────────────────┐
│         Kubernetes Control Plane            │
│                                             │
│  API Server → etcd (Service Registry)       │
└─────────────────┬───────────────────────────┘
                  │
┌─────────────────┴───────────────────────────┐
│         Kubernetes Nodes                    │
│                                             │
│  ┌─────────────────────────────────────┐   │
│  │  kube-proxy (iptables/IPVS)         │   │
│  │                                     │   │
│  │  payment-service.default.svc.cluster.local
│  │         ↓                           │   │
│  │  10.96.100.50 (ClusterIP)          │   │
│  │         ↓                           │   │
│  │  ├─> Pod 1: 10.244.0.5:8080        │   │
│  │  ├─> Pod 2: 10.244.1.6:8080        │   │
│  │  └─> Pod 3: 10.244.2.7:8080        │   │
│  └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

Service 타입:

ClusterIP (기본값):

apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  type: ClusterIP
  selector:
    app: payment
  ports:
    - port: 80
      targetPort: 8080

작동 원리:

  1. Endpoint Controller: Pod IP 자동 추적
  2. kube-proxy: iptables/IPVS 규칙 생성
  3. CoreDNS: 서비스 이름 → ClusterIP 변환
  4. 로드 밸런싱: iptables가 Pod 선택

DNS 조회:

# 같은 네임스페이스
curl http://payment-service/api/charge
 
# 다른 네임스페이스
curl http://payment-service.production.svc.cluster.local/api/charge

장점:

  • 설정 불필요 (자동 제공)
  • 플랫폼 수준 통합
  • 강력한 헬스 체크 (Liveness/Readiness Probe)

단점:

  • Kubernetes 환경에서만 사용 가능
  • 커스터마이징 제한적

4. etcd

분산 키-값 저장소

Kubernetes의 백엔드로 사용되는 고신뢰성 데이터 스토어

특징:

  • Raft 알고리즘: 강력한 일관성 보장
  • Watch 기능: 변경 사항 실시간 감지
  • TTL 지원: 자동 만료 처리

사용 예시:

# 서비스 등록
etcdctl put /services/payment/instance1 '{"host":"10.0.1.5","port":8080}'
 
# 서비스 조회
etcdctl get /services/payment --prefix
 
# Watch (변경 감지)
etcdctl watch /services/payment --prefix

장점:

  • 강력한 일관성
  • 빠른 성능
  • 간단한 API

단점:

  • 직접 사용 시 헬스 체크 등 추가 구현 필요
  • 주로 인프라 레벨에서 사용

5. Apache Zookeeper

분산 코디네이션 서비스

Kafka, Hadoop 등에서 사용되는 성숙한 솔루션

특징:

  • ZAB 알고리즘: 강력한 일관성
  • 계층적 네임스페이스: 디렉토리 구조
  • Watcher: 변경 알림

구조:

/services
  /payment
    /instance1 → {"host":"10.0.1.5","port":8080}
    /instance2 → {"host":"10.0.1.6","port":8080}
  /order
    /instance1 → {"host":"10.0.2.3","port":8081}

장점:

  • 검증된 안정성
  • CP (Consistency & Partition tolerance)
  • 많은 레퍼런스

단점:

  • 설정 및 운영 복잡
  • 상대적으로 무거움

📊 구현체 비교

구현체패턴일관성 모델주요 사용처난이도
KubernetesServer-SideCP컨테이너 환경
EurekaClient-SideAPSpring Boot⭐⭐
ConsulBothCP멀티 클라우드⭐⭐⭐
etcd-CPKubernetes 백엔드⭐⭐⭐
Zookeeper-CPKafka, Hadoop⭐⭐⭐⭐

5. 실제 구현

💻 Spring Cloud Eureka 예시

1. Eureka Server 구성

의존성 (pom.xml):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

애플리케이션 클래스:

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

설정 (application.yml):

server:
  port: 8761
 
eureka:
  instance:
    hostname: localhost
  client:
    # Eureka Server 자신은 클라이언트로 등록하지 않음
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  server:
    # 자가 보호 모드 (운영환경에서는 true 권장)
    enableSelfPreservation: false
    # Eviction 주기
    evictionIntervalTimerInMs: 5000

2. Eureka Client 구성 (서비스 제공자)

의존성:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

애플리케이션 클래스:

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentServiceApplication.class, args);
    }
}

설정:

spring:
  application:
    name: payment-service  # 서비스 이름 (중요!)
 
server:
  port: 8080
 
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
    # Registry 정보 가져오기
    fetchRegistry: true
    # 자신을 등록
    registerWithEureka: true
  instance:
    # IP 주소 우선 사용
    preferIpAddress: true
    # 인스턴스 ID
    instance-id: ${spring.application.name}:${random.value}
    # Heartbeat 간격
    leaseRenewalIntervalInSeconds: 30
    # 만료 시간
    leaseExpirationDurationInSeconds: 90

3. Eureka Client 구성 (서비스 소비자)

REST 호출 방법 1: DiscoveryClient 직접 사용

@Service
public class OrderService {
 
    @Autowired
    private DiscoveryClient discoveryClient;
 
    @Autowired
    private RestTemplate restTemplate;
 
    public PaymentResponse processPayment(PaymentRequest request) {
        // payment-service의 모든 인스턴스 조회
        List<ServiceInstance> instances =
            discoveryClient.getInstances("payment-service");
 
        if (instances.isEmpty()) {
            throw new ServiceNotFoundException("payment-service");
        }
 
        // 첫 번째 인스턴스 선택 (실제로는 로드밸런싱 필요)
        ServiceInstance instance = instances.get(0);
        String url = String.format("http://%s:%d/api/charge",
            instance.getHost(), instance.getPort());
 
        return restTemplate.postForObject(url, request, PaymentResponse.class);
    }
}

REST 호출 방법 2: @LoadBalanced + Ribbon (권장)

@Configuration
public class RestTemplateConfig {
 
    @Bean
    @LoadBalanced  // Ribbon 로드 밸런싱 활성화
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
 
@Service
public class OrderService {
 
    @Autowired
    private RestTemplate restTemplate;
 
    public PaymentResponse processPayment(PaymentRequest request) {
        // 서비스 이름으로 호출 (Ribbon이 자동으로 인스턴스 선택)
        String url = "http://payment-service/api/charge";
        return restTemplate.postForObject(url, request, PaymentResponse.class);
    }
}

REST 호출 방법 3: Spring Cloud OpenFeign (가장 권장)

@FeignClient(name = "payment-service")
public interface PaymentClient {
 
    @PostMapping("/api/charge")
    PaymentResponse charge(@RequestBody PaymentRequest request);
}
 
@Service
public class OrderService {
 
    @Autowired
    private PaymentClient paymentClient;
 
    public PaymentResponse processPayment(PaymentRequest request) {
        // 선언적 방식으로 간단하게 호출
        return paymentClient.charge(request);
    }
}

🚀 Kubernetes Service Discovery 예시

1. Deployment + Service 생성

# payment-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
  labels:
    app: payment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: payment
  template:
    metadata:
      labels:
        app: payment
    spec:
      containers:
      - name: payment
        image: mycompany/payment-service:1.0
        ports:
        - containerPort: 8080
        # 헬스 체크 설정
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
---
# payment-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: payment-service
spec:
  type: ClusterIP
  selector:
    app: payment  # Deployment의 label과 매칭
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP

2. 서비스 호출

같은 네임스페이스:

@Value("${payment.service.url:http://payment-service}")
private String paymentServiceUrl;
 
public PaymentResponse charge(PaymentRequest request) {
    String url = paymentServiceUrl + "/api/charge";
    return restTemplate.postForObject(url, request, PaymentResponse.class);
}

다른 네임스페이스:

// FQDN 사용
String url = "http://payment-service.production.svc.cluster.local/api/charge";

3. 확인 명령어

# Service 확인
kubectl get svc payment-service
 
# Endpoint 확인 (실제 Pod IP 목록)
kubectl get endpoints payment-service
 
# DNS 조회 테스트
kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup payment-service

6. 장단점

✅ 장점

  1. 동적 확장성

    • Auto Scaling 시 자동으로 인스턴스 추가/제거
    • 수동 설정 변경 불필요
  2. 장애 격리

    • 헬스 체크로 비정상 인스턴스 자동 제외
    • 빠른 장애 복구
  3. 환경 독립성

    • 동일한 코드로 모든 환경 지원
    • 서비스 이름만으로 접근
  4. 로드 밸런싱

    • 자동 부하 분산
    • 다양한 알고리즘 지원 (Round Robin, Random, Weighted 등)
  5. 유지보수 용이

    • 중앙화된 서비스 관리
    • 실시간 서비스 토폴로지 파악

❌ 단점

  1. 추가 인프라 필요

    • Service Registry 운영 필요
    • HA 구성 필요
  2. 복잡도 증가

    • 학습 곡선
    • 디버깅 어려움
  3. 네트워크 의존성

    • Registry 장애 시 영향
    • 네트워크 지연 추가 가능
  4. 일관성 이슈

    • Registry 동기화 지연
    • 캐시 불일치 가능성

7. 사용 시기

✅ 적합한 경우

  1. 클라우드 네이티브 환경

    • Kubernetes, AWS ECS 등
    • Auto Scaling 사용
  2. 마이크로서비스 아키텍처

    • 서비스 수가 5개 이상
    • 서비스 간 통신 빈번
  3. 동적 환경

    • 인스턴스가 자주 변경
    • 배포가 빈번
  4. 멀티 리전/데이터센터

    • 지리적 분산 배포
    • 글로벌 서비스

❌ 부적합한 경우

  1. 정적 환경

    • 서버 IP가 거의 변경되지 않음
    • On-premise 고정 서버
  2. 소규모 시스템

    • 서비스 1-2개
    • 단순한 아키텍처
  3. 레거시 시스템

    • 기존 DNS/하드코딩 방식 사용
    • 변경 비용이 큼

8. 2026년 표준 스택

🏆 권장 구성

컨테이너 환경 (Kubernetes):

Kubernetes Service (CoreDNS)
  ↓
자동 제공, 추가 설정 불필요

Spring Cloud 환경:

Eureka Server (HA 구성)
  ↓
Eureka Client + Ribbon/OpenFeign
  ↓
Resilience4j Circuit Breaker

멀티 클라우드 환경:

Consul Cluster
  ↓
Consul Agent (각 서비스)
  ↓
DNS/HTTP API

🚀 2026년 트렌드

  1. Service Mesh 통합

    • Istio, Linkerd가 Service Discovery 내장
    • 애플리케이션 코드 수정 불필요
  2. Serverless 환경

    • AWS Lambda + API Gateway
    • 자동 서비스 디스커버리
  3. Edge Computing

    • CDN 레벨 라우팅
    • 지리적 최적화

9. 실전 사례

🏢 Netflix

규모:

  • 수천 개의 마이크로서비스
  • 수백만 인스턴스

스택:

  • Eureka Server (자체 개발)
  • Ribbon (Client-side LB)
  • Zuul (API Gateway)

성과:

  • 99.99% 가용성
  • 글로벌 배포 자동화

🏢 Uber

사용 사례:

  • 실시간 위치 기반 서비스
  • 수백 개의 마이크로서비스

스택:

  • Custom Service Discovery
  • gRPC + Load Balancing
  • Multi-DC 지원

성과:

  • 밀리초 단위 응답
  • 지역별 최적화

📚 참고 자료

🔗 관련 패턴

📖 추가 학습 자료


상위 문서: 통신 패턴 폴더 마지막 업데이트: 2026-01-05 다음 학습: Circuit Breaker 패턴


Supported by Sonnet 4.5