앞서 Chapter 1에서 MSA 도입 시 고려할 점에서 'Service Discovery 패턴'이라는 개념이 등장했었습니다.
이번 챕터에서는 Service Discovery 패턴에 대해 알아보고
해당 패턴을 구현한 Spring Cloud Netflix의 Eureka를 제 개인 프로젝트에 적용한 과정을 담아보도록 하겠습니다.
1. Service Discovery 패턴이란?
Service Discovery 패턴은 MSA를 도입하면서 불편한 점을 해결하는 패턴입니다.
어떠한 패턴인지를 알아보기 위해서는 MSA 도입 시 어떠한 점이 불편한지를 알아보면 됩니다.
이 부분은 Chapter 1에서 다뤘던 부분이므로 해당 부분을 인용하겠습니다.
앞서 MSA에서는 외부에서 오는 요청은 API Gateway를 통해서 전달되고, 서비스 간 통신도 이루어진다고 했습니다.
이때 API Gateway는 어떻게 서비스 Application의 정보를 알아서 전달하고,
각 서비스들도 어떻게 다른 서비스 Application의 정보를 알아서 통신할까요?
간단히 생각해보면 API Gateway에서 모든 서비스 Application의 정보를 수동으로 등록하고
각 서비스에서도 다른 서비스들의 정보를 수동으로 등록하면 간단히 해결됩니다.
그러나, 애플리케이션을 운영하다 보면 사용하지 않는 서비스, 추가해야하는 서비스가 있을 수 있습니다.
이렇게 됐을 때 서비스가 확장/축소 될 때마다 수동으로 API Gateway, 각 서비스들의 정보들을 업데이트 해야합니다.
요약해보면, MSA를 기본적으로 도입할 때
각각의 마이크로 서비스가 확장/축소될 때마다 해당 서비스의 정보(IP, Port)를 수동으로 업데이트 해야하는 점이 불편한 점입니다.
요즘은 클라우드 환경 도입에 따라서 동적으로 서버가 Scale-out 되는 일이 많습니다.
이러한 상황에서 여러 서버의 정보를 수동으로 업데이트한다는 것은 매우 불편할 것입니다.
이러한 불편한 점을 해결하는 것이 바로 Service Discovery 패턴입니다.
Service Discovery 패턴은 다음과 같은 역할을 수행합니다.
서비스의 위치(IP, Port 등)를 저장 및 관리하는 서비스의 주소록 역할
여기서 'Service Registry'라는 개념이 등장하는데, 다음과 같습니다.
서비스의 위치(IP, Port 등)를 저장 및 관리하는 서비스의 주소록
따라서, Service Discovery 패턴은 Service Registry를 구성하여 구현하는 것입니다.
이렇게 Service Discovery 패턴을 구현함으로써 서비스를 호출하는 쪽에서는 서비스의 위치를 몰라도
Service Discovery를 구현한 구현체에게 서비스의 위치를 질의함으로써 요청을 전달할 수 있습니다.
외부에서 API Gateway를 통한 요청과 서비스간 통신 모두 Service Discovery를 거쳐서 요청을 전달하게 됩니다.
2. Server-Side Discovery / Client-Side Discovery
이러한 Service Discovery 패턴에는 2가지 종류가 존재합니다.
- Server-Side Discovery
- Client-Side Discovery
2가지를 모두 알아보고, 제 프로젝트에는 어떤 것을 적용했는지 설명드리겠습니다.
알아보기 전에, Client-Side와 Server-Side를 나누는 기준은 다음과 같습니다.
MSA의 다른 서비스를 호출할 때 Service Registry를 통해서 다른 서비스를 호출하는가?
결론은 다음과 같습니다.
- Server-Side Discovery : Service Registry가 아닌 앞단의 Load Balancer를 통해 다른 서비스 호출
- Client-Side Discovery : Service Registry를 통해 서비스 호출
2-1. Server-Side Discovery
Server-Side Discovery는 앞서 말한 것처럼 다른 서비스를 호출할 때 Service Registry를 거치지 않고
앞단의 물리적인 로드 밸런서를 배치하여 로드 밸런서가 내부의 Service Registry에게 질의함으로써 결과를 반환하는 Flow입니다.
이러한 Server-Side Discovery의 장단점을 살펴봅시다.
장점
- 각 서비스들이 다른 서비스를 호출할 때 Load Balancer에 요청을 보내도록 설계되므로 Service Registry의 구체적인 구현은 모른채로 Load Balancer만 호출하면 된다. (추상화된 Load Balancer만 호출하고, 하위 구현체인 Service Registry는 몰라도 된다, 즉 캡슐화되어 있다.)
- 또, 추상화된 Load Balancer에 요청만 보내면 되므로 다른 서비스를 검색하는 로직을 구현할 필요가 없다.
단점
- 배포 환경에서 앞단의 로드 밸런서를 배치하는 것이 필수적이다.
- 요청 시 Load Balancer를 한 단계 거쳐야하므로 네트워크 hop이 증가하여 처리가 상대적으로 지연될 수 있다.
2-2. Client-Side Discovery
Client-Side Discovery는 앞서 말한 것처럼 각 서비스들이 Service Registry에게 질의하여 결과를 반환하는 Flow입니다.
Client-Side Discovery에서는 Service Registry를 구현하는 라이브러리들을 사용합니다.
대표적인 라이브러리로는 다음과 같은 라이브러리가 존재합니다.
- Spring Cloud Netflix Eureka : Service Registry의 서버 역할, 서비스들을 등록하는 역할
이러한 Client-Side Discovery의 장단점을 살펴봅시다.
장점
- 인프라 작업인 앞단에 Load Balancer를 배치하는 작업을 하지 않고, 애플리케이션 코드 단에서 Service Discovery 패턴을 구현할 수 있어서 상대적으로 간단하다.
- 각 서비스들이 호출하려는 서비스를 알기 때문에 서비스별로 특성에 맞게 로드밸런싱 방식을 구현할 수 있다.
단점
- 각 서비스별로 다른 서비스를 검색하는 로직을 언어 및 프레임워크 별로 구현해야 한다.
- 따라서, 각 서비스가 Service Registry에 의존적이다.
- 만약 서비스별로 동일한 언어 및 프레임워크가 아니라 다른 언어 및 프레임워크 환경인 폴리그랏(polyglot) 환경이라면 언어 및 프레임워크별로 여러번 구현해야한다.
3. Eureka를 사용한 Client-Side Discovery 구현
앞서 Server-Side Registry와 Client-Side Registry의 개념, 장단점을 살펴봤습니다.
결론적으로 제 프로젝트에서는 Client-Side Registry를 적용하기 위해
Eureka 라이브러리를 사용하여 Service Discovery를 구현했습니다.
다음과 같은 이유로 Client-Side Discovery를 구현하게 되었습니다.
- 인프라 작업이 아닌 애플리케이션 코드 단 작업이라서 상대적으로 접근이 쉽고 간단하다.
- 레퍼런스가 Eureka를 사용하는 Client-Side Discovery가 압도적으로 많다.
- 현재 프로젝트도 여러 언어 및 프레임워크를 사용하는 것이 아닌 Java & Spring으로만 구성되어 있다.
그럼 이제 본격적으로 Eureka를 사용하여 Service Discovery를 구현해보겠습니다.
최종적으로 구현할 Eureka Client-Side Discovery는 다음과 같은 구조입니다.
간략하게 위의 구조를 설명하면 다음과 같습니다.
- Eureka Server 라이브러리를 추가하여 Service Registry를 구현
- 각 마이크로 서비스에서 Eureka Client 라이브러리를 추가하여 Eureka Server에 서비스 정보 등록
- 각 서비스 애플리케이션 실행 시 Eureka Server에 자동으로 서비스 정보 등록
- 각 서비스에서는 주기적(default : 30초)으로 Eureka Server에게서 다른 서비스들의 정보를 업데이트
- 등록 시 고유 ID를 지정하여 서비스 호출 시 IP, Port가 아닌 고유 ID로 서비스 호출 가능
3-1. Eureka Server 구현
이제 각 서비스들의 정보를 관리하는 Eureka Server를 구현해보도록 하겠습니다.
3-1-1. build.gradle Eureka Server 의존성 추가
ext {
set('springCloudVersion', "2023.0.0")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
위와 같이 eureka-server 의존성을 추가해줍니다.
spring actuator도 유틸성 기능을 활용하기 위해 추가해줬습니다.
3-1-2. Eureka Server 관련 application.yml 설정
server:
port: 8761
spring:
application:
name: discovery-server
eureka:
client:
register-with-eureka: false
fetch-registry: false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
# 서버가 요청을 받기 전 대기할 초기 시간(ms)
# default 5분 : 실무에서는 보통 모든 서비스가 등록 되길 기다리기 위해 5분 후 정보를 공유
# 개인 프로젝트인 만큼 빠른 등록을 위해 5ms로 설정
wait-time-in-ms-when-sync-empty: 5
management:
endpoints:
web:
exposure:
include: "*"
- server.port : Eureka Server의 Port를 지정합니다.
- spring.application.name : Eureka Server의 고유 ID(Name)를 지정합니다.
- eureka.client.register-with-eureka : Eureka Server로 자신의 정보를 등록할지 결정합니다.
- 현재는 Eureka Server 설정이므로 자기 자신을 등록할 필요는 없기 때문에 false로 설정했습니다.
- eureka.client.fetch-registry : 다른 서비스들의 정보를 Eureka Server로부터 받아서 로컬 메모리에 캐싱할지 여부입니다.
- 현재는 Eureka Server 설정이므로 자기 자신의 정보를 캐싱할 필요는 없기 때문에 false로 설정했습니다.
- eureka.client.serviceUrl.defaultZone : Eureka Server의 URL을 지정합니다. 이후에 Eureka API 요청 시 해당 URL로 요청을 보낼 수 있습니다.
- eureka.server.wait-time-in-ms-when-sync-empty : 각 Service들의 등록을 위한 대기 시간입니다. 해당 시간동안 대기한 후에 등록된 Service들의 정보를 공유합니다.
- management.endpoints.web.exposure.include : Spring Actuator 관련 설정으로, Actuator의 어떤 API를 사용할 것인지 지정하는 설정입니다. 우선은 '*'으로 모든 API를 사용하고 이후에 지정해보도록 하겠습니다.
3-1-3. Eureka Server 활성화
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
실행 Application 위에 @EnableEurekaServer 어노테이션을 선언하면, Eureka Server 관련 구성이 활성화되어
Eureka Server를 활성화할 수 있습니다.
이렇게 설정을 마치고 DiscoveryServerApplication을 실행하고
localhost:설정한 Eureka Server Port(8761)의 URI로 접속해보면 다음과 같은 Eureka Server 대시보드가 렌더링됩니다.
이렇게 대시보드가 렌더링되면, Eureka Server의 설정은 끝입니다.
3-2. Eureka Clients 구현
이제 각 Service들에서 다른 서비스들의 정보를 얻기 위한 Eureka Clients를 구현해보도록 하겠습니다.
아래의 과정은 MSA의 각 서비스들에서 모두 Eureka Clients를 추가해주면 됩니다.
제 프로젝트의 경우에는 Member-Service와 Board-Service에 모두 Eureka Clients를 추가해줍니다.
설정이 거의 동일하기 때문에 Member-Service 기준으로 포스팅하겠습니다.
3-2-1. build.gradle Eureka Clients 의존성 추가
ext {
set('springCloudVersion', "2023.0.0")
}
dependencies {
// eureka-client 외의 서비스에 필요한 의존성들
...
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
위와 같이 eureka-client 의존성을 추가해줍니다.
spring actuator도 유틸성 기능을 활용하기 위해 추가해줬습니다.
3-2-2. Eureka Client 관련 application.yml 설정
server:
port: 8011
spring:
application:
name: member-service
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
management:
endpoints:
web:
exposure:
include: "*"
일부 설정은 Eureka Server의 설정과 동일하므로 했던 설명은 생략하고 추가적인 부분만 살펴보겠습니다.
- spring.application.name : Eureka 환경에서 통신할 서비스의 이름을 지정하는 옵션입니다.
- Eureka 환경에서 통신을 할 때는 서비스의 IP:Port가 아닌, 고유 ID로 통신을 하게 됩니다.
- 이때, 통신할 고유 ID를 지정하는 옵션입니다.
- eureka.instance.prefer-ip-address : 서비스의 호스트 이름이 아닌, IP 주소를 Eureka Server에 등록할지 여부입니다.
- 기본값은 false로, 해당 옵션을 true로 설정하지 않으면 Eureka Server에 각 서비스들이 내부적으로 호스트 이름으로 등록됩니다.
- 서비스가 배포되는 환경에서 DNS가 존재한다면 호스트 이름을 할당받고 Eureka Server가 잘 등록할 수 있으므로 해당 옵션을 설정할 필요가 없습니다.
- 하지만, 컨테이너 기반의 배포 환경이라면 컨테이너가 DNS 엔트리가 없는 임의의 호스트 이름이 부여되기 때문에 해당 옵션을 false로 했을 때 Eureka Server가 해당 서비스의 호스트 이름 위치를 정상적으로 얻지 못합니다.
- 따라서, 컨테이너 기반 배포라면 해당 옵션을 true로 하여 Eureka Server가 서비스를 IP 주소로 등록해서 찾도록 해야 합니다.
- eureka.client.register-with-eureka : Eureka Server에 자기 자신을 등록할지의 여부이므로 true를 지정해줬습니다.
- eureka.client.fetch-registry : Eureka Server의 등록되어 있는 서비스들을 캐싱해둘지의 여부이므로 true를 지정해줬습니다.
3-1-3. Eureka Client 활성화
@EnableDiscoveryClient
@SpringBootApplication
public class MemberServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MemberServiceApplication.class, args);
}
}
실행 Application 위에 @EnableDiscoveryClient 어노테이션을 선언하면, Eureka Client 관련 구성이 활성화되어
Eureka Client를 활성화할 수 있습니다.
4. 최종 Eureka Server 대시보드 확인
앞서 Eureka Server, Eureka Client(Member-Service, Board-Service)의 설정을 마무리했습니다.
이제 로컬 환경에서 EurekaServer, Eureka Client들을 실행하고 Eureka Server 대시보드를 확인해봅시다.
멀티 모듈 환경에서 다음과 같이 애플리케이션 3개를 실행해줬습니다.
http://localhost:8761로 접속해보면, 다음과 같이 등록된 Member-Service, Board-Service의 정보들을 확인할 수 있습니다.
이상으로, Eureka 구현을 마무리해보도록 하겠습니다.
🎯 Github Repository 링크 (전체 코드)
https://github.com/sh111-coder/sh-board-msa
📘 Monolithic to MSA 전체 목차
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란?
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (2) 멀티 모듈 구성하기
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (3) Service Discovery 패턴 적용하기(feat. Spring Cloud Eureka)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (4) API Gateway 구현(feat. Spring Cloud Gateway)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (5) 서비스 간 통신하기(feat.Spring Cloud OpenFeign)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (6) 각 서비스의 설정 파일 관리하기(feat. Spring Cloud Config)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (7) 서비스 장애 대응 Circuit Breaker 구현(feat. Resilience4J)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (10) MSA 전환 후 비교 및 회고 + 마무리
Reference
https://enjoy-dev.tistory.com/10
https://enjoy-dev.tistory.com/2
'아키텍쳐' 카테고리의 다른 글
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (5) 서비스 간 통신하기 (feat. Spring Cloud OpenFeign) (1) | 2024.02.06 |
---|---|
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (4) API Gateway 구현(feat. Spring Cloud Gateway) (0) | 2024.02.05 |
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (2) 멀티 모듈 구성하기 (1) | 2024.02.02 |
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란? (0) | 2024.02.01 |
Controller와 Service 레이어 간 요청 & 응답 Dto 사용에 관하여 (2) | 2023.06.19 |