1. 들어가며
마이크로서비스 아키텍처를 도입하면, 애플리케이션은 하나의 덩어리가 아니라 여러 개의 독립된 서비스로 쪼개지게 된다. 이 각각의 서비스는 독립적으로 배포되고, 개별적으로 확장 가능하다는 장점이 있지만, 반대로 각 서비스가 서로를 찾고 통신해야하는 문제가 생긴다. 이 때, 이 문제를 해결해주는 Spring Cloud Eureka와 Service Discovery 개념에 대해서 정리해보려고 한다.
1.1 마이크로서비스에서 주소 관리는 왜 복잡해질까?
근데, 왜 마이크로서비스에서 주소 관리는 복잡해진다고 할까? 서비스를 여러 개로 쪼개면 다음과 같은 문제가 생긴다.
어떤 서비스가 다른 서비스를 호출할 때 정확한 주소(IP:PORT)를 알아야 한다
그런데 이 주소는 환경마다 다르고, 특히 클라우드 환경에선 컨테이너가 재시작되거나 Auto Scaling이 동작하면서 IP나 포트가 수시로 바뀐다. 수동으로 이걸 관리한다는 건 거의 불가능에 가깝다. 이런 문제를 해결하기 위해 서비스 디스커버리(Service Discovery)를 도입하게 된다.
2. 서비스 디스커버리란?
서비스 디스커버리는 말 그대로 서비스를 동적으로 찾을 수 있도록 도와주는 기능이다.
이를 위해 보통 다음 세 가지 역할이 존재한다.

- Service Registry: 서비스의 주소 정보를 저장하고 관리하는 중앙 저장소. (Eureka Server)
- Service Provider: 자신의 주소를 Registry에 등록하는 서비스. (예: user-service)
- Service Consumer: Registry에서 주소를 조회해 다른 서비스를 호출하는 주체. (예: order-service)
2.1 Eureka란 무엇인가?
Spring Cloud에서 지원하는 대표적인 서비스 디스커버리 도구가 바로 Eureka다. Netflix에서 만든 오픈소스 프로젝트로, Spring Cloud와 매우 잘 통합되어 있어 쉽게 사용할 수 있다.
Eureka는 Service Registry 역할을 하고, 클라이언트들은 Eureka에 자신을 등록하거나 다른 서비스를 조회하는 방식으로 동작한다.
또한 자체적으로 Health Check, Self-preservation 모드, 복제(Replication) 같은 고급 기능도 제공한다.
3. Eureka Server 실습 예제
Eureka를 쓰기 위해선 먼저 서비스 주소를 저장하고 관리할 Eureka Server를 만들어야 한다.
3.1 의존성 추가
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
}
3.2 설정과 어노테이션
Eureka 서버는 자기 자신을 등록할 필요가 없기 때문에 아래처럼 설정한다.
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
그리고 어플리케이션에 @EnableEurekaServer를 붙이면 끝이다.
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
서버를 실행하면 http://localhost:8761 에서 Eureka 대시보드를 확인할 수 있다.
4. Eureka Client 만들기
4.1 의존성 추가
이제 실제로 서비스들(user-service, order-service 등)이 Eureka에 자신을 등록하고, 다른 서비스를 찾을 수 있게 설정해보자.
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
4.2 설정 예시
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
여기서 중요한 건 spring.application.name이다. 이 이름이 곧 Eureka에 등록되는 서비스 식별자가 된다.
5. 서비스 간 통신은 어떻게?
Eureka에 등록했으니 이제 서비스 간에 직접 IP나 포트를 쓰지 않고도 서로를 호출할 수 있다.
5.1 RestTemplate + Eureka
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
restTemplate.getForObject("http://user-service/api/users", String.class);
여기서 user-service는 Eureka에 등록된 서비스 이름이다.
@LoadBalanced를 붙여줌으로써, RestTemplate이 Eureka와 통합되어 주소를 동적으로 가져올 수 있다.
5.2 FeignClient 사용
더 간결하고 선언적인 방식으로는 FeignClient를 사용할 수 있다.
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/api/users")
List<User> getUsers();
}
Feign은 내부적으로 Eureka를 조회해서 동적으로 통신을 처리해주기 때문에 매우 깔끔한 방식이다.
6. Eureka의 추가 기능
6.1 인스턴스 상태 관리
Eureka는 기본적으로 Self-preservation 모드가 켜져 있어서, 서비스가 잠깐 네트워크 문제로 다운되어도 바로 제거하지 않는다.
statusPageUrl, healthCheckUrl 같은 메타데이터도 함께 등록할 수 있어 상태 모니터링에 도움이 된다.
6.2 Eureka Replication
Eureka는 여러 대로 구성하여 서로 레지스트리를 복제할 수 있다.
고가용성(HA)를 위해서 실무에선 2대 이상 Eureka 서버를 두기도 한다고 한다.
7. 마무리
Eureka는 작은 규모의 Spring 기반 마이크로서비스 아키텍처에서 간단한 설정으로 동적 서비스 탐색을 구현할 수 있는 장점이 있다.
하지만 Kubernetes 같은 클라우드 네이티브 환경에서는 Service Mesh나 DNS 기반 디스커버리 방식이 더 일반적이긴 하다.
그래도 Spring 환경에서는 여전히 매우 실용적인 기술이다. 특히 Gateway, Config, Feign, Bus 등과 연계해서 사용하면 진짜 마이크로서비스처럼 유기적으로 움직이는 시스템을 만들 수 있다. 다음은 spring cloud gateway에 대해서 정리해보겠다.