Service Discovery — Eureka로 서비스를 등록하고 찾는 원리
서비스가 스케일 아웃으로 3개에서 5개로 늘어났는데, 다른 서비스들은 새로 생긴 인스턴스의 주소를 어떻게 알 수 있을까요?
전통적인 방식에서는 서비스 주소를 설정 파일에 하드코딩했습니다. 하지만 클라우드 환경에서 인스턴스는 수시로 생성되고 소멸합니다. Service Discovery는 서비스 이름만으로 실제 네트워크 위치를 자동으로 찾아주는 메커니즘입니다.
Service Discovery란
Service Discovery는 서비스의 네트워크 위치(IP, 포트)를 동적으로 관리 하는 패턴입니다.
두 가지 방식이 있습니다.
Client-Side Discovery
클라이언트가 레지스트리에서 서비스 목록을 가져와 직접 로드밸런싱합니다. Eureka가 이 방식입니다.
Client → [Registry 조회] → 인스턴스 목록 → 로드밸런싱 → 서비스 호출
Server-Side Discovery
로드 밸런서가 레지스트리를 조회하고 라우팅합니다. Kubernetes Service가 이 방식입니다.
Client → LoadBalancer → [Registry 조회] → 서비스 호출
Eureka Server 구성
의존성
<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 (Eureka Server — 단일 인스턴스)
server:
port: 8761
eureka:
client:
register-with-eureka: false # 자기 자신은 등록하지 않음
fetch-registry: false # 레지스트리를 가져오지 않음
server:
enable-self-preservation: true # Self-Preservation 활성화
Eureka Client 구성
의존성
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
클라이언트 설정
# application.yml (각 서비스)
spring:
application:
name: order-service # Eureka에 이 이름으로 등록
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
registry-fetch-interval-seconds: 30 # 레지스트리 캐시 갱신 주기
instance:
prefer-ip-address: true # 호스트명 대신 IP 사용
lease-renewal-interval-in-seconds: 30 # heartbeat 주기
lease-expiration-duration-in-seconds: 90 # 이 시간 동안 heartbeat 없으면 제거
서비스가 시작되면 자동으로 Eureka Server에 등록됩니다.
동작 원리
서비스 등록
- 서비스가 시작되면 Eureka Server에 자신의 정보(이름, IP, 포트, 상태)를 등록
- Eureka Server가 레지스트리에 인스턴스 정보를 저장
Heartbeat
- 클라이언트가 30초(기본)마다 heartbeat를 보냄
- 서버가 90초(기본) 동안 heartbeat를 받지 못하면 인스턴스를 제거
레지스트리 조회
- 클라이언트가 30초(기본)마다 서버에서 레지스트리를 가져와 로컬에 캐시
- 서비스 호출 시 로컬 캐시에서 대상 인스턴스를 찾아 호출
order-service-1 ──heartbeat──→ [Eureka Server]
order-service-2 ──heartbeat──→ [레지스트리]
order-service-3 ──heartbeat──→
↓
payment-service ←── 레지스트리 캐시 ──┘
(order-service 호출 시 캐시에서 인스턴스 목록 조회)
서비스 간 통신 — 로드밸런싱
Eureka에 등록된 서비스를 호출할 때 @LoadBalanced RestTemplate이나 WebClient를 사용합니다.
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
@LoadBalanced가 붙은 WebClient는 서비스 이름을 Eureka에서 조회하여 실제 인스턴스 주소로 변환합니다.
@Service
public class OrderService {
private final WebClient.Builder webClientBuilder;
public UserInfo getUserInfo(Long userId) {
return webClientBuilder.build()
.get()
.uri("http://user-service/api/users/{id}", userId) // 서비스 이름으로 호출
.retrieve()
.bodyToMono(UserInfo.class)
.block();
}
}
http://user-service는 Eureka에 등록된 서비스 이름입니다. Spring Cloud LoadBalancer가 자동으로 인스턴스 목록에서 하나를 선택하여 실제 IP/포트로 변환합니다.
Self-Preservation 모드
Self-Preservation은 Eureka의 안전장치입니다.
왜 필요한가
네트워크 파티션이 발생하면 정상적인 서비스의 heartbeat도 도달하지 못합니다. 이때 Eureka가 "heartbeat가 없으니 인스턴스를 제거하자"라고 판단하면, 실제로는 살아있는 서비스까지 레지스트리에서 제거되어 대규모 장애로 이어집니다.
동작 방식
heartbeat 갱신 비율이 임계값(기본 85%) 아래로 떨어지면 Self-Preservation 활성화:
- ** 인스턴스를 제거하지 않음** (보수적 판단)
- Eureka 대시보드에 경고 메시지 표시
- 네트워크가 복구되면 자동 해제
eureka:
server:
enable-self-preservation: true
renewal-percent-threshold: 0.85 # 85% 이하면 활성화
개발 환경에서는 enable-self-preservation: false로 끄는 것이 편하지만, ** 운영 환경에서는 반드시 활성화 **해야 합니다.
고가용성 — Peer Replication
Eureka Server를 여러 대 운영하면 레지스트리가 자동으로 동기화됩니다.
# Eureka Server 1
eureka:
client:
service-url:
defaultZone: http://eureka-2:8762/eureka/
instance:
hostname: eureka-1
# Eureka Server 2
eureka:
client:
service-url:
defaultZone: http://eureka-1:8761/eureka/
instance:
hostname: eureka-2
# 클라이언트 — 두 서버 모두에 등록
eureka:
client:
service-url:
defaultZone: http://eureka-1:8761/eureka/,http://eureka-2:8762/eureka/
하나의 Eureka Server가 다운되어도 다른 서버가 서비스하고, 클라이언트의 로컬 캐시로 인해 영향이 최소화됩니다.
Kubernetes 대안
Kubernetes 환경에서는 Eureka 없이도 Service Discovery가 가능합니다.
# Kubernetes Service
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 8080
Kubernetes의 Service 리소스가 자동으로 DNS 이름(order-service.namespace.svc.cluster.local)을 생성하고, 로드밸런싱을 제공합니다.
| 기준 | Eureka | Kubernetes Service |
|---|---|---|
| 인프라 의존성 | 별도 서버 필요 | K8s 내장 |
| 디스커버리 방식 | Client-Side | Server-Side (kube-proxy) |
| Health Check | 앱 레벨 heartbeat | kubelet probe |
| 비K8s 환경 | 사용 가능 | 불가 |
K8s 환경이라면 Spring Cloud Kubernetes를 사용하여 네이티브 디스커버리를 활용하는 것이 추세입니다.
실무 팁
- ** 로컬 캐시 **를 활용하므로 Eureka Server 장애 시에도 기존 통신은 유지됩니다
lease-expiration-duration-in-seconds를 너무 짧게 설정하면 일시적 네트워크 문제로도 인스턴스가 제거될 수 있습니다- Eureka 대시보드(
http://eureka:8761)에서 등록된 인스턴스와 상태를 확인하세요 - ** 헬스체크 **를 Eureka에 통합하여 비정상 인스턴스가 트래픽을 받지 않도록 설정하세요
주의할 점
1. 레지스트리 캐시 갱신 주기(기본 30초) 때문에 인스턴스 변경이 즉시 반영되지 않는다
Eureka 클라이언트는 30초마다 레지스트리를 갱신합니다. 새 인스턴스가 등록되거나 기존 인스턴스가 제거되어도 다른 서비스가 이를 인지하기까지 최대 30초가 걸립니다. 이 지연 동안 이미 내려간 인스턴스로 요청이 갈 수 있으므로, 서킷 브레이커나 재시도 로직을 함께 적용해야 합니다.
2. Self-Preservation 모드가 활성화되면 죽은 인스턴스가 레지스트리에 남아있는다
네트워크 불안정 시 Self-Preservation이 활성화되면 Eureka가 인스턴스를 제거하지 않습니다. 실제로 내려간 서비스가 레지스트리에 남아 있어 해당 인스턴스로 요청이 라우팅될 수 있습니다. 개발 환경에서는 Self-Preservation을 끄는 것이 편하지만, 운영에서는 클라이언트 측 헬스체크를 병행해야 합니다.
3. Eureka Server가 단일 인스턴스면 SPOF(단일 장애점)가 된다
Eureka Server가 다운되면 새 인스턴스 등록이 불가능하고, 기존 클라이언트도 레지스트리를 갱신할 수 없습니다. 로컬 캐시 덕분에 기존 통신은 유지되지만, 스케일 아웃/인이 불가능해집니다. 운영 환경에서는 반드시 2대 이상의 Eureka Server를 Peer Replication으로 구성하세요.
정리
- Service Discovery 는 동적 환경에서 서비스의 네트워크 위치를 자동으로 관리합니다
- Eureka는 heartbeat + 레지스트리 캐싱 방식으로 동작합니다
- Self-Preservation 은 네트워크 파티션 시 과도한 인스턴스 제거를 방지하는 안전장치입니다
- Kubernetes 환경에서는 내장 Service/DNS가 Eureka를 대체할 수 있습니다