우리는 Spring에서 지원해주는 프레임워크 중 하나인 Spring Cloud를 활용하여
이전에 살펴본 MSA를 구현해볼 수 있다.
Spring Cloud Netflix
스프링 클라우드 넷플릭스는 Spring Boot Application에 관한 통합 환경 구성을 지원하는 Netflix의 OSS 서비스이다.
- OSS: 오픈 소스 소프트웨어
Netflix OSS와 Spring Cloud Netflix
Netflix OSS에는 다양한 서비스들이 존재한다.
각각에 관해 간략히 알아보자.
- Eureka : Discovery Server
- 각각의 서비스 인스턴스들이 동적으로 확장, 축소 되더라도 인스턴스의 상태를 하나의 서비스로 관리할 수 있는 서비스
- Ribbon: Client Side Load Balancer
- 부하 분산을 위한 모놀리스의 L4 스위치와 같이 트래픽을 분산시키는 기능
- OpenFeign에서 Ribbon을 활용하여 다른 MS들과 통신
- Zuul: API Gateway
- 각각의 마이크로서비스의 종단점을 연결하는 리버스 프록시
- Spring Boot에 Zuul이 있었으나 deprecated 된걸로 알음
- Hystrix: Circuit Breaker
- 특정 서비스가 과부하가 걸려 서비스 장애를 전파하는 특성을 해결하려는 기능
- EvCache
- Spectator
- Archaius
알아본 것처럼 Netflix OSS에는 굉장한 많은 서비스가 존재한다.
하지만, 이런 서비스들이 모두 Spring Cloud에서 사용되진 않는다.
Spring은 Spring Cloud와 잘 맞는 몇몇 서비스들을 Spring Cloud에 맞게 재구성하여 제공한다.
최초에는 모든 서비스가 Spring Cloud에 의해서 관리되었다고 한다.
하지만, 현재는 몇몇 기술들을 재구성 or 발전시켜 독립적인 서비스로 진화시켰다.
그럼 가장 기본이 되는 Spring Cloud Netflix - Eureka부터 알아보자.
Spring Cloud Netflix - Eureka
이전 글에서 살펴보았듯 MSA의 Service Mesh 패턴에서 API Gateway는 Service Router에게 어디로 가야할지 묻는다.
그것에 관한 대답을 Service Discovery가 해준다. (= Service Discover Pattern)
Netflix-Eureka
즉, Service Discovery는 MSA 핵심 요소 중 하나이다.
각 서비스의 설정 및 위치 정보 (IP:PORT)를 수동으로 구성한다면 CI/CD 혹은 Scaling에 매우 많은 제약사항이 따른다.
하지만, Netflix-Eureka는 서버를 구성 및 배포하여 가용성이 높도록 할 수 있다.
이러한 Eureka의 개념에는 2가지 구성 요소가 존재한다.
- Service Discovery
- 각각의 서비스의 위치가 등록된 서버에서 특정 작업을 위한 서버의 위치를 파악하는 작업
- 우리는 Service Discovery를 위해 Spring Cloud Netflix - Eureka Server를 사용
- Service Registry
- 각각의 서비스가 자신의 위치(IP) 정보를 특정 서버에 등록 Registry 하는 작업을 말한다
- 우리는 Service Registry를 위해 Spring Cloud Netflix - Eureka Client를 사용
Service Discover 과정
Spring Cloud Eureka 다음과 같은 순서로 동작한다.
- Service Registry 기능을 할 Eureka Server가 최초에 기동
- Service Registry 서버인 Eureka Server에 등록될 서비스들이 기동(Eureka Client)
- Eureka 서버는 자신에게 등록된 Eureka Client에게 30초마다 Ping을 보내 Health Checking을 수행
- 만약, 30초마다 보내는 Heart Heat가 일정 횟수 이상으로 동작되지 않으면 Eureka Server는 해당 Client 삭제
이때, 중요한점은 Eureka는 단지 서비스의 위치만을 표현하는 역할로 Spring Cloud Gateway와 같은 Gateway 서비스나
Ribbon과 같은 클라이언트 사이드 로드밸런서와 함께 동작한다.
EurekaServer 설정 예시
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
@SpringBootApplication
@EnableEurekaServer // 추가
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
EurekaServer yml 설정 예시
server:
port: 8761
spring:
application:
name: discovery-service
eureka:
client:
register-with-eureka: false
fetch-registry: false
EurekaClient 설정 예시
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
@SpringBootApplication
@EnableDiscoveryClient // 추가
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
}
EurekaClient yml 설정 예시
eureka:
instance:
instance-id: user-microservice-instance
hostname: localhost
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
다음으로는 가볍게 API Gateway Flow를 알아보자.
Spring Cloud Gateway는 다음과 같은 순서로 동작한다.(Gateway도 Eureka Client로 등록되어 있음)
- 클라이언트(React)가 Gateway로 Request 요청 전송
- Spring Cloud Gateway는 MS의 IP:PORT를 정보를 요청
- 해당 MS로 라우팅 및 로드밸런싱을 하여 클라이언트 요청 전달
- MS의 응답을 다시 클라이언트로 전달
Gateway에 관해 좀 더 자세하게 알아보자
Gateway Pattern
MSA에서 Gateway 패턴은 가장 흔하고 유용하게 쓰는 패턴 중 하나이다.
API Gateway는 RESTful 하게 작성된 모든 서비스의 API를
손쉽게 관리하여 자원의 효율적인 분배를 수행한다.
MSA는 서비스를 여러 개의 프로젝트로 분리한다.
그럼 사용자 즉, 웹 브라우저에 요청된 사용자의 정보 및 요청은 어떤 서비스가 맡아야할까?
당연히 적절한 서비스이다.
여기서 등장하는 것이 바로 API Gateway 이다.
API Gateway는 아래에 있는 3가지 문제의 해결책이 될 수 있다.
- 해당 요청이 어떻게 적절한지 판단을 할까?
- 모든 서비스의 요청에는 공통된 인증 과정을 수행해야 하는데, 어디서 인증을 수행할까?
- 따로 인증 서버를 둬야할까?
- 그렇다면 API의 모든 요청에 대한 로그 파일을 작성하고 싶다면 어떻게 해야할까?
- 각각의 서비스마다 로깅 서버를 둬야 하나?
API Gateway라는 개념을 활용하면 이를 하나의 프로젝트에서 관리한다.
(하지만, 내부 서비스들 각각에도 로그 처리를 해주어야 내부 서비스의 자세한 오류 확인이 용이하다.)
API Gateway의 역할
API Gateway에는 대표적으로 다음과 같은 역할이 존재
- 프록시(리버스 프록시)의 역할과 로드밸런싱
- 인증 서버로서의 기능
- 로깅 서버로서의 기능
위의 동작 과정에서 모든 데이터가 하나의 서버로 전송되므로 해당 서버는 인증/로깅을 수행하기도 한다.
또한, 리버스 프록시 역할을 수행하기 때문에 로드밸런싱과 라우팅이 가능해진다.
API Gateway의 고려사항
API Gateway가 무조건 장점만 있는것은 아니고 몇 가지 단점이 존재한다.
병목 현상
만약 서비스가 100개가 존재한다면 API Gateway는 가장 앞단에서
100개의 서비스가 요청될 수 있는 트래픽을 감당해야 한다.
그럼 당연하게도 병목 현상이 발생하며, 전체적인 서비스의 성능이 느려질 수 있다.
(적절한 Scale-Out이 필요하다)
네트워크 Latency
네트워크 Latency는 하나의 데이터 패킷을 한 지점에서 다른 지점으로 보내는데 소요되는 시간을 말한다.
당연하게도 API Gateway라는 네트워크를 한 번 더 타기 때문에 네트워크 지연 현상이 발생한다.
여기까지가 Gateway이 간략한 설명이다.
다시 Spring Cloud Gateway로 돌아가보자.
Spring Cloud Gateway
Spring Cloud Gateway는 Netty 서버를 내장한 Web Flux 기반으로 기존 비동기 통신 로직에 취약한 Tomcat을 대신한다.
즉, Spring WebFlux위에서 API Gateway를 구축하기 위한 라이브러리이다.
Spring Cloud Gateway는 간편하면서도 효과적인 API 라우팅, 보안 모니터링, 메트릭, 복원력 문제를 해결하는 것이 목표다.
Gateway를 도입한다면 다음과 같은 구조가 될 것이다.
위의 그림은 User 서비스가 Order 서비스에 어떠한 요청을 보내는 상황이다.
- User 서버는 Order 서버에 보내야할 요청을 Gateway로 전달
- Gateway는 Eureka Server로 Order 서버의 정보 discovery
- Gateway가 Order Server로 연결
위의 순서로 진행되며 이것은 다음의 장점을 갖고온다.
- 각각의 마이크로서비스들은 서로의 포트 번호를 몰라도 된다.
- Front에서는 요청을 Gateway로만 보내면 되기 때문에 Gateway 포트만 알면 모든 요청 수행이 가능하다.
- 모든 요청은 Gateway를 거치기 때문에 로그를 쉽게 다룰 수 있다.
- Gateway가 요청의 진입점이므로 통합 인증 수행이 가능.
Spring Cloud Gatewa에서 사용하는 3가지 명칭
Spring Cloud Gateway에서 사용하는 3가지 용어는 다음과 같다.
- Route
- Predicate
- Filter
Route (라우트)
목적지의 URI와 Predicates라는 조건들의 목록 그리고 필터들을 이용해 어떤 곳으로 Routing 할 것인지를 명시한다.
Routes에 관한 속성과 정보를 입력해 Spring Cloud가 어떤 방식으로 동작하라고 지시할 수 있다.
Predicate
쉽게 조건으로 생각하면 된다.
간단하게 Predicates는 다음과 같이 작성한다.
predicated: -Path=/user/**
그럼 user라는 모든 경로를 지정한 것이다.
Filter(필터)
들어오는 요청과 응답, Request, Response을 특정 필터를 타게 함으로
우리가 원하는 방식으로 요청을 보내거나 헤더를 조작할 수 있다.
또한, 해당 필터를 이용해 로그 파일을 작성하게 할 수도 있다.
Spring Cloud Gateway 동작 과정
- 클라이언트는 Spring Cloud Gateway에 요청을 보낸다.
- Gateway Handler Mapping에서 해당 요청에 관한 Route와 Predicates가 일치한다고 판단하면 해당 요청은 Gateway Web Handler로 보내진다.
- handler에서 Filter Chain을 이용해 사전 필터 혹은 사후 필터로 나누어 동작한다.
- 필터링이 된 후 실제 마이크로서비스에게 전달된다.
Spring Cloud Gateway 내부를 좀 더 자세히 알아보자
Spring Cloud Gateway는 크게 3가지의 파트가 존재한다.
- Gateway Handler Mapping
- Gateway가 클라이언트로 부터 어떤 요청이 왔는지 확인하고 Mapping 하는 작업을 수행.
- Spring의 Dispatcher Servlet과 비슷.
- Predicate
- Handler Mapping 시에 필요한 URI 정보나, Path 정보를 확인하는 주체.
- Filter
- Handler Mapping이 된 후 들어온 요청에 관한 필터 작업 수.
- Pre Filter
- 특정 작업이 일어나기 전에 지정
- Post Filter
- 특정 작업이 끝난 후에 지정
- 이런 필터 정보는 yml 설정 파일에도 정의가 가능하며 java code에서도 정의가 가능하다.
Predicate 예시
predicates:
- Path=/user/**
Filter 예시
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter(){
super(Config.class);
}
@Override
public GatewayFilter apply(Config config){
//Custom Pre Filter 적용
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("GLOBAL FILTER baseMessage: {}", config.getBaseMessage());
if(config.isPreLogger())
log.info("GLOBAL FILTER START: 요청 path -> {}", request.getPath());
//Custom Post Filter 적용
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
//Mono -> Spring Webflux 기능: 동기 방식이 아니라 비동기 방식의 서버를 지원할 때 단일 값 전달 활용 때 사용
if(config.isPostLogger())
log.info("GLOBAL FILTER END: 응답 code -> {}", response.getStatusCode());
}));
});
}
@Getter
@Setter
public static class Config{
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
Filter yml 설정 예시
filters:
- RewritePath=/user/?(?<segment>.*), /$\{segment}
# - CustomAuthFilter
- name: AuthorizationFilter
args:
headerName: Authorization
granted: Bearer
Spring Cloud Gateway yml 설정 예시
server:
port: 8000
spring:
application:
name: gateway
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
- id: user-service
uri: lb://USER-SERVICE # 포워딩할 주소, http://localhost:8000/user 로 들어오면 http://localhost:64412 로 포워딩
predicates:
- Path=/user/** # 해당 gateway 서버의 /user/**로 들어오는 요은 user-service로 인식하겠다는 조건
filters:
- RewritePath=/user/?(?<segment>.*), /$\{segment}
# - CustomAuthFilter
- name: AuthorizationFilter
args:
headerName: Authorization
granted: Bearer
- id: team-service
uri: lb://TEAM-SERVICE # 포워딩 할 주소, http://localhost:8000/team 로 들어오면 http://localhost:54412 로 포워딩
predicates:
- Path=/team/** # 해당 gateway 서버의 /order/**로 들어오는 요청은 order-service로 인식하겠다는 조건
filters:
- RewritePath=/team/?(?<segment>.*), /$\{segment}
- id: todo-service
uri: lb://TODO-SERVICE
predicates:
- Path=/api/todos/**
filters:
- RewritePath=/api/todos/?(?<segment>.*), /$\{segment}
여기까지 API Gateway에 관해 알아보았다.
아마 이쯤되면 각 서비스는 API Gateway로 통신을 하는데 과연 어떤 방법을 사용하여 통신을 할지 궁금해질 것이다.
그와 관련된 설명을 진행하겠다.
Microservices에서 서비스간의 통신
마이크로서비스에서는 하나의 기능을 수행하기 위해 각각의 마이크로서비스 인스턴스들이
유기적인 상호작용을 하여 적절한 데이터를 사용자에게 내려준다.
보통의 MSA에서 각각의 서비스는 RESTful 한 API를 제공하는데,
이 때 각 서비스는 특정 서비스가 노출하는 Endpoint에 API 호출을 하여 데이터를 조작한다.
대표적인 MSA 통신 방식은 2가지이다.
1. Rest Template(HTTP 1.1)
2. Spring Cloud OpenFeign(HTTP 1.1)
하지만, 나는 프로젝트에서 OpenFeign을 사용할 것으로 OpenFeign에 관한 설명을 진행한다.
OpenFeign 선정 이유는 좀 더 설정이 간편하고 익숙하기 때문이다.
또한, RestTemplate의 경우는 URI 경로가 호출 로직 자체에 존재하기 때문에 가독성이 떨어진다.
MSA가 서비스간 통신 구현도
build.gradle에 아래 의존성을 추가하자.
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
또한, Application.java로 가서 @EnableFeignClients 어노테이션을 추가하자.
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class EurekaClientUserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientUserServiceApplication.class, args);
}
}
TeamService에 접근하기 위한 Client 인터페이스 생성
@FeignClient(name = "team-service")
public interface TeamServiceClient {
@GetMapping("/{userId}/teams")
TeamResponseData getTeam(@PathVariable("userId") Long id);
}
OpenFeign과 RestTemplate 비교
이름 | 코드 가독성, 직관성 | 예외 처리 | 테스트 용이성 | 러닝 커브 |
OpenFeign | 가독성 좋음 | ErrorDecoder 제공 | 일반적인 인터페이스의 간편한 stubbing | 낮음 |
RestTemplate | 가독성이 좋게 되기 위해 다른 작업 필요 | try-catch | Spring 이 구현해놓은 객체의 복잡한 stubbing | 낮음 |
직접적인 프로젝트 구성에 관해서는 나중에 프로젝트 카테고리에서 따로 다루겠다.
참고 블로그
'Spring > Cloud' 카테고리의 다른 글
Spring With Kafka (0) | 2024.05.05 |
---|---|
Spring Cloud란? (0) | 2024.03.07 |
Cloud Native, 12 factors, MSA (2) | 2024.03.07 |