가볍게 마이크로서비스 구축해보기-2
Netflix Eureka를 이용해 Service Discovery 패턴 구현하기
지난 마이크로서비스 아키텍처 소개에 이어 이번 포스트에서는 서비스 디스커버리(Service Discovery) 패턴에 대한 내용을 다루고, Netflix Eureka를 이용해 구현해보도록 하겠습니다.
(포스트 게시일자 기준 Medium 블로그에 접속 시 GitHup Gist를 통해 삽입된 소스 코드가 보이지 않는 버그가 있었습니다. 참고하시기 바랍니다.)
서비스 디스커버리
마이크로서비스 아키텍처(이하 MSA)를 구성하는 서비스들은 각각 별개의 네트워크 위치 정보(IP, Port)를 갖습니다. 이 서비스들의 위치 정보는 클라우드 같은 환경에서 동적으로 바뀔 수 있고, 예기치 못한 장애로 인해 특정 네트워크 위치 정보의 서비스는 이용이 불가능할 수도 있습니다.
따라서, 이런 분산 시스템에서는 서비스들의 네트워크 위치 정보를 관리하고 가용 상태를 확인해줄 수 있는 기능이 필요로 한데, 이때 해결책으로 제시될 수 있는 디자인 패턴이 서비스 디스커버리입니다.
위 그림과 같이 서비스 디스커버리는 서비스의 네트워크 위치 정보와 서비스의 가용 상태를 관리할 수 있는 서비스 레지스트리(Service Registry)를 두고, 클라이언트가 서비스 레지스트리에게 가용 상태에 있는 서비스의 네트워크 위치 정보를 질의하게 함으로써 구현할 수 있습니다.
이러한 서비스 디스커버리는 크게 클라이언트 사이드 디스커버리(Client-Side Discovery)와 서버 사이드 디스커버리(Server-Side Discovery)로 구현 방식이 나눠집니다. 클라이언트 사이드 디스커버리는 클라이언트가 직접 서비스 레지스트리에 질의하는 방식으로, 앞서 처음 설명했던 구현 방식이 이에 해당합니다. 반면, 서버 사이드 디스커버리는 클라이언트와 서버 레지스트리 사이에 로드 밸런서(Load Balancer)를 위치시키는 방식으로, 로드 밸런서가 클라이언트 대신 서비스 레지스트리에 질의하여 서비스를 호출하는 방식입니다.
클라이언트 사이드 디스커버리는 클라이언트가 서비스 레지스트리에 직접 질의해야 하는 구조에서 발생하는 의존성 때문에, 일반적인 클라이언트(사용자)와 서버의 관계에서는 보통 서버 사이드 디스커버리로 구현합니다. 하지만, MSA에서 서비스 간에 통신을 위한 목적이라면, 클라이언트 사이드 디스커버리 또한 고려해볼만한 방식입니다.
Netflix Eureka
Eureka는 Netflix OSS(Netflix Open Source Software)에 포함된 컴포넌트들 중 하나로써 서비스 디스커버리 패턴을 구현할 수 있습니다. Eureka 서버(서비스 레지스트리)와 Eureka 클라이언트로 구성되어져 있으며, Spring Cloud Netflix에서 제공하는 패턴에도 포함되어 있습니다.
그럼 지금부터 Spring Cloud Netflix에서 제공하는 Eureka를 통해 클라이언트 사이드 디스커버리를 구현해보도록 하겠습니다.
(서버 사이드 디스커버리에 대한 구현은 API Gateway를 다루는 포스트에서 다뤄볼 예정입니다.)
구현 시나리오
서비스 레지스트리는 독립형(Standalone)으로, 마이크로서비스는 Service-A와 Service-B 두 개의 서비스로 구성하겠습니다. 또한 Service-B는 가용성을 높이기 위해 2개의 인스턴스로 배포하고, Service-B의 API가 호출될 시 라운드 로빈(Round Robin)을 기반으로 Service-B의 인스턴스가 선택되도록 하겠습니다.
Eureka 서버 구현 (서비스 레지스트리)
1. 프로젝트 템플릿 다운로드
Spring Initializr에서 ‘Eureka Server’를 검색하여 의존성(Dependencies)에 추가하고 프로젝트 템플릿을 다운 받아 IDE에 임포트합니다.
2. application.yml 설정
- eureka.client.register-with-eureka
: Eureka 서버에 클라이언트로 등록 여부 - eureka.client.fetch-registry
: Eureka 서버로부터 받은 서비스 리스트에 대한 캐싱여부
3. @EnableEurekaServer 활성화
Eureka 서버로써 동작하기 위해 @SpringBootApplication이 활성화된 메인 애플리케이션 클래스에 @EnableEurekaServer를 추가합니다.
4. Eureka 대시보드 확인
이제 프로젝트를 실행시키고 localhost:8761에 접속하여 Eureka 대시보드를 확인합니다. 대시보드의 ‘Instances currently registered with Eureka’ 섹션을 통해 Eureka 서버에 등록된 서비스 인스턴스 정보를 확인할 수 있습니다.
Eureka 클라이언트 구현 (Service-A, Service-B)
1. 프로젝트 템플릿 다운로드
Spring Initializr에서 ‘Eureka Discovery Client’, ‘Spring Web Starter ’를 검색하여 의존성에 추가하고 프로젝트 템플릿을 다운 받아 IDE에 임포트합니다.
2. application.yml 설정
- eureka.instance.instance-id
: 동일한 서비스내에서 인스턴스를 식별하기 위한 ID - eureka.client.service-url.defaultZone
: Eureka 클라이언트가 속할 Zone 정의
3. @EnableDiscoveryClient, @LoadBalenced 활성화
Eureka 클라이언트로써 동작하기 위해 @SpringBootApplication이 활성화된 메인 애플리케이션 클래스에 @EnableDiscoveryClient를 추가합니다.
또한, 서비스간 통신을 위해 RestTemplate을 이용하고 @LoadBalenced을 함께 활성화 합니다. @LoadBalenced가 활성화 되면, Eureka에 내장된 로드밸런서인 Ribbon을 이용해 라운드 로빈을 기반으로 서비스 인스턴스를 호출하게 됩니다.
4. 서비스간 API 호출을 위한 REST API 추가
- Service-A의 송신측 API
: Service-B의 API를 호출하기 위한 호스트와 포트 정보 대신 Eureka 서버에 등록된 Service-B의 애플리케이션 네임을 사용합니다.
- Service-B의 수신측 API
5. Eureka 대시보드 확인
이제 Eureka 클라이언트 프로젝트를 실행시키면, Eureka 서버의 대시보드를 통해 서비스 인스턴스가 등록된 것을 확인할 수 있습니다.
시나리오 검증
HTTP 클라이언트를 이용해 Service-A의 /api/rpc/test를 호출해보겠습니다.
호출된 Service-A의 API는 Eureka 서버에 등록된 Service-B의 애플리케이션 네임과 API의 URL 경로(api/healthcheck)만을 가지고, Service-B의 인스턴스들을 라운드 로빈 기반으로 호출하고 있는 것을 확인할 수 있습니다.
지난 포스트를 보신 분들은 이번 포스트에서 서비스간 REST API 기반으로 통신하는 것을 보시고 의문을 가지실 수도 있습니다. 지난 포스트에서 제가 서비스들간의 통신은 비동기로 구현하여 서비스들 간에 의존성이 발생하지 않게 해야 한다고 했기 때문입니다.
맞습니다!, 서비스들간의 통신은 되도록 비동기로 구현하는 것이 이상적입니다. 하지만, 현실에서는 비즈니스 요구사항이나 혹은 시스템이 처음 의도한 방향과는 다르게 확장이 되면서 어쩔 수 없이 서비스 간에 동기로 통신을 해야 하는 상황이 발생하곤 합니다. 따라서, 서비스 간의 통신은 비동기뿐만아니라, 동기 통신까지 고려하여 설계하는 것이 좋습니다.
다음 포스트에서는 Netflix Zuul(API Gateway)을 이용해 서버 사이드 디스커버리를 구현해보도록 하겠습니다.
피드백은 언제나 환영입니다!