🔍 서비스 디스커버리 패턴
패턴 개요
서비스 디스커버리는 마이크로서비스 환경에서 서비스 인스턴스의 네트워크 위치(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) │
└────────────────┘ └────────────┘
동작 흐름:
- 등록 (Registration): 서비스 인스턴스가 시작되면 자신의 정보를 레지스트리에 등록
- 조회 (Discovery): 클라이언트가 호출할 서비스의 위치를 레지스트리에서 조회
- 헬스 체크 (Health Check): 레지스트리가 주기적으로 서비스 상태 확인
- 해제 (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-Side | Server-Side |
|---|---|---|
| 제어 위치 | 클라이언트 | 인프라 |
| 로드밸런싱 | 클라이언트가 선택 | 로드밸런서가 선택 |
| 복잡도 | 클라이언트 복잡 | 클라이언트 단순 |
| 네트워크 홉 | 1 hop | 2 hops |
| 플랫폼 의존성 | 라이브러리 필요 | 독립적 |
| 대표 예시 | Netflix Eureka | Kubernetes |
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작동 원리:
- Endpoint Controller: Pod IP 자동 추적
- kube-proxy: iptables/IPVS 규칙 생성
- CoreDNS: 서비스 이름 → ClusterIP 변환
- 로드 밸런싱: 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)
- 많은 레퍼런스
단점:
- 설정 및 운영 복잡
- 상대적으로 무거움
📊 구현체 비교
| 구현체 | 패턴 | 일관성 모델 | 주요 사용처 | 난이도 |
|---|---|---|---|---|
| Kubernetes | Server-Side | CP | 컨테이너 환경 | ⭐ |
| Eureka | Client-Side | AP | Spring Boot | ⭐⭐ |
| Consul | Both | CP | 멀티 클라우드 | ⭐⭐⭐ |
| etcd | - | CP | Kubernetes 백엔드 | ⭐⭐⭐ |
| Zookeeper | - | CP | Kafka, 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: 50002. 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: 903. 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: TCP2. 서비스 호출
같은 네임스페이스:
@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-service6. 장단점
✅ 장점
-
동적 확장성
- Auto Scaling 시 자동으로 인스턴스 추가/제거
- 수동 설정 변경 불필요
-
장애 격리
- 헬스 체크로 비정상 인스턴스 자동 제외
- 빠른 장애 복구
-
환경 독립성
- 동일한 코드로 모든 환경 지원
- 서비스 이름만으로 접근
-
로드 밸런싱
- 자동 부하 분산
- 다양한 알고리즘 지원 (Round Robin, Random, Weighted 등)
-
유지보수 용이
- 중앙화된 서비스 관리
- 실시간 서비스 토폴로지 파악
❌ 단점
-
추가 인프라 필요
- Service Registry 운영 필요
- HA 구성 필요
-
복잡도 증가
- 학습 곡선
- 디버깅 어려움
-
네트워크 의존성
- Registry 장애 시 영향
- 네트워크 지연 추가 가능
-
일관성 이슈
- Registry 동기화 지연
- 캐시 불일치 가능성
7. 사용 시기
✅ 적합한 경우
-
클라우드 네이티브 환경
- Kubernetes, AWS ECS 등
- Auto Scaling 사용
-
마이크로서비스 아키텍처
- 서비스 수가 5개 이상
- 서비스 간 통신 빈번
-
동적 환경
- 인스턴스가 자주 변경
- 배포가 빈번
-
멀티 리전/데이터센터
- 지리적 분산 배포
- 글로벌 서비스
❌ 부적합한 경우
-
정적 환경
- 서버 IP가 거의 변경되지 않음
- On-premise 고정 서버
-
소규모 시스템
- 서비스 1-2개
- 단순한 아키텍처
-
레거시 시스템
- 기존 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년 트렌드
-
Service Mesh 통합
- Istio, Linkerd가 Service Discovery 내장
- 애플리케이션 코드 수정 불필요
-
Serverless 환경
- AWS Lambda + API Gateway
- 자동 서비스 디스커버리
-
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 지원
성과:
- 밀리초 단위 응답
- 지역별 최적화
📚 참고 자료
🔗 관련 패턴
- API Gateway 패턴 - 외부 통신 관리
- Service Mesh 패턴 - 내부 통신 고도화
- Circuit Breaker 패턴 - 장애 격리
📖 추가 학습 자료
상위 문서: 통신 패턴 폴더 마지막 업데이트: 2026-01-05 다음 학습: Circuit Breaker 패턴