MSA 구현을 목표로 Kafka와 MSA에 대해 학습하던 중, 스프링 클라우드라는 것을 알게 되었다.
그래서 스프링 클라우드에 대해 알아보고, 이를 이용해 실제 MSA를 어떻게 구성하면 되는지 학습해보는 시간을 가지려고 한다.
1️⃣ Spring Cloud (스프링 클라우드)
스프링 클라우드란, 클라우드 네이티브 애플리케이션 개발을 도와주는 스프링 기반 프레임워크 이다.
- 클라우드 네이티브 애플리케이션는 클라우드 환경에서 실행되도록 설계된 프로그램을 말하며, 클라우드 컴퓨팅의 장점인 민첩성과 확장성을 최대한 활용하도록 설계 되어있다.
- 다양한 기능을 제공하여, 마이크로 서비스 아키텍처를 쉽게 구현하고 운영할 수 있도록 도와준다.
- 디스커버리, 로드 밸런싱, 서킷 브레이커, 게이트웨이 등등 다양한 기능을 제공한다.
📌 스프링 클라우드 주요 모듈
1. Eureka
- 디스커버리 서버로, 모든 서비스 인스턴스(클라이언트)의 위치를 저장하는 중앙 저장소 역할을 한다. (미들웨어서버)
- 뿐만 아니라, 서비스 인스턴스의 상태를 주기적으로 관리하는 헬스 체크도 수행한다.
- IP, Port, Instance ID를 가지며 REST 기반으로 작동한다.
2. Ribbon
넷플릭스가 개발한 클라이언트 사이드 로드 밸런서로, 서비스 인스턴스 간의 부하를 분산한다.
- Eureka로 부터 서버 인스턴스 리스트를 제공받아서 로드 밸런싱에 사용한다.
- 라운드 로빈, 가중치 기반 등 다양한 로드 밸렁싱 알고리즘을 선책할 수 있으며, 요청 실패 시 다른 인스턴스로 자동으로 전환하여 요청하는 Failover도 지원한다.
💡 서버 사이드 로드밸런싱 vs 클라이언트 사이드 로드 밸런싱
기존의 로드 밸런싱는 대부분 서버 사이드 방식으로, L4 Switch와 같은 하드웨어 장비를 사용했습니다. 별도의 하드웨어 장치가 필요하고, 스위치의 서버 목록 추가가 수동으로 이루어져야 한다는 단점이 있습니다. 그래서 이를 보완한 것이 클라이언트 사이드 로드 밸런싱이다. 클라이언트가 직접 여러 서버 중 하나를 선택하여 요청을 보내는 방식으로 클라이언트가 서버 목록을 가지고 있어야 한다. 하지만 별도의 하드웨어적인 요소가 필요하지 않기 때문에 MSA에서는 해당 방식을 채택하고 있다.
3. Hystrix(넷플릭스), Resilience4j
서킷 브레이커 라이브러리로, 서비스 간의 호출 실패를 감지하고 시스템의 안정성을 유지시키는 역할을 한다.
- 서킷 브레이커 상태인 closed, open, half-open으로 상태를 나타내며 호출 실패를 관린하다.
- 타임아웃 설정, 호출 실패시 대체 로직을 수행하는 기능(Failback)등을 제공하여 시스템의 안정성을 확보한다.
- 재시도 기능을 활용함으로써, 일시적인 네트워크 문제에도 대응할 수 있도록 지원하다. (Resilience4j)
- Resilience4j는 Hystrix의 대안으로 등장한 경량 서킷 브레이커 라이브러리이다.
4. Zuul(넷플릭스), Cloud Gateway
Gateway 역할로, 클라이언트로 요청을 받고 이를 서버 인스턴스에 매핑하여 전달하는 역할을 한다.
- 클라이언트의 요청을 받는 곳인 만큼, filter를 구성한 있어 인증 및 인가 역할도 맡고 있다.
- 이외에도 서버간의 요청도 관리하기 때문에 로드 밸런싱 기능을 가지고 있고, 모니터링 또한 가능하다.
5. Spring Cloud Config
분산된 서버 인스턴스들의 config를 관리하는 역할로, 분산된 환경에서 중앙 집중식 설정 관리를 지원하다.
- config 서버를 지정하고, 서버 인스턴스들을 config 클라이언트로 등록하여 관리한다.
- 설정 변경 시, 서비스 재시작 없이 실시간으로 반영한다.
이러한 모듈들을 활용하여 MSA를 구성하며, 대략적인 구조도는 아래와 같다.
2️⃣ Eureka 서버 구성하기
1. 의존성 주입
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
2. Application.class
@SpringBootApplication
@EnableEurekaServer // 해당 어노테이션 추가하기
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
- `@EnableEurekaServer`을 Application 클래스에 넣어준다.
3. application.yml
spring:
application:
name: server
server:
port: 19090
eureka:
client:
register-with-eureka : false
fetch-registry : false
service-url:
defaultZone: http://localhost:19090/eureka/
instance:
hostname: localhost
- `register-with-eureka` : 유레카 서버에 자신을 등록할지 여부를 나타내는 것으로, 유레카 클라이언트에만 true로 설정한다.
- `fetch-registry` : 유레카 서버로부터 레지스토리를 가져올지를 나타내는 것으로, 클라이언트가 아니기 때문에 false를 설정한다.
- `instance.hostname` : 유레카 서버 인스턴스의 호스트 이름 설정
- `client.service-url.defaultZone` : 유레카 클라이언트가 서버와 통신하기 위해 사용할 기본 url
설정을 완료한 상태에서 실행후, 접속해보면 아래와 같이 Eureka 서버 정보가 뜬다. 해당 페이지에서 클라이언트로 인식된 서버 인스턴스들을 확인할 수 있다.
➕ Eureka 이중화
// Eureka 서버 1의 application.yml
eureka:
client:
serviceUrl:
defaultZone: http://1.1.1.1:19090/eureka/
// Eureka 서버 2의 application.yml
eureka:
client:
serviceUrl:
defaultZone: http://2.2.2.2:19090/eureka/
// Eureka 클라이언트 설정
eureka:
client:
serviceUrl:
defaultZone: http://1.1.1.1:19090/eureka/, http://2.2.2.2:19090/eureka/
Eureka 이중화란, Eureka 서버가 사용불능 상태가 되었을때를 대비해서 여러개의 Eureka 서버를 띄우는 것이다.
- 서버는 위와 같이 각 인스턴스 마다 host를 다르게 설정하거나, port 번호를 다르게 설정하여 이중화를 할 수 있다.
- 클라이언트는 Eureka 서버 url을 설정하는 곳에, 모든 url을 설정하면, Failover 기능을 지원받는다.
- 클라이언드 사이드 로드밸런싱이기 때문에, Eureka 서버간의 동기화 및 duplication은 필요가 없다.
3️⃣ Eureka 서버에 서버 인스턴스 등록하기
서버 인스턴스 등록은 RestTemplate, FeignClient로 할 수 있는데, 저는 후자로 진행했습니다.
1. 의존성 주입
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' //Ribbon
2. Application.class
@SpringBootApplication
@EnableFeignClients
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
- `@EnableFeignClients`를 application 클래스에 넣어준다.
3. application.yml
spring:
application:
name: product-service
server:
port: 19092
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
- `application.name` : 서버 인스턴스 이름을 지정하는 것으로, 서버 간의 통신시 해당 이름으로 요청을 주고 받는다.
- `client.service-url.defaultZone` : 등록할 Eureka 서버의 url을 작성한다.
설정을 완료한 뒤, Eureka 서버와 서비스 서버를 순차대로 실행하면 아래와 같이 Eureka 페이지에 인스턴스로 서비스 서버가 뜨는 것을 확인할 수 있다.
4️⃣ 서버 인스턴스 간의 요청 주고 받기
Order 서비스가 Product 서비스에 요청하여 Product 정보를 받고, 이 정보로 주문을 만든다고 가정해보자.
Order 프로젝트
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/product/{id}")
String getProduct(@PathVariable("id") String id);
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final ProductClient productClient;
public String getProductInfo(String productId) {
return productClient.getProduct(productId);
}
public String getOrder(String orderId) {
if(orderId.equals("1") ){
String productId = "2";
String productInfo = getProductInfo(productId);
return "Your order is " + orderId + " and " + productInfo;
}
return "Not exist order...";
}
}
위와 같이 FeignClient를 활용해 인터페이스를 구현하여 Order프로젝트에서 Product 프로젝트에 요청을 보낼 수 있다.
- `@FeginClient`을 통해 product-sevice라는 이름의 인스턴스를 찾고, 매핑되는 요청을 반환하는 것이다.
- 이후 해당 인터페이스를 빈에 등록하여, 평소 service을 사용하는 것처럼 활용하면된다.
5️⃣ 로드 밸런싱 직접 확인하기
FeginClient에서 의존성 주입시, Ribbon도 함께 넣어주었다. Ribbon은 요청한 서버의 인스턴스가 여러개 일때 동작하며, 설정된 로드 밸런싱 알고리즘을 통해 요청을 분배한다. (클라이언트 사이드 로드 밸런싱임을 잊지 말자)
Order 서버 1개와 Product 서버 3개(port : 19092, 19093, 19094)를 운영하는 상황이라고 가정하자.
이 상태에서 위에서 보았던 getOrder()요청을 3번 수행하면, 아래와 같이 동작한다.
Product Server가 19092 - 19093 - 19094로 바뀌는 것을 확인할 수 있다.
- 기본값인, 라운드 로빈으로 동작하여 여러번의 요청은 각각 다른 서버 인스턴스로 분배된 것을 확인할 수 있다.
- 라운드 로빈 알고리즘 이외에도, 가중치 기반 로드 밸린성, 최소 연결 및 응당 시간 기반 밸런싱 등 다양한 알고리즘이 존재한다.