3개월의 인턴기간을 거쳤던 회사는 MSA 구조를 사용했었다.
모놀리식 구조로만 프로젝트를 진행해 왔었지만 MSA 라는것이 어떤것인지만 대강 알고 있었고
실제로 구현해본 적은 없었기 때문에 실제로 구현해 볼수 있어서 좋은 경험이였다.
모놀리식 구조에선 쇼핑몰을 예로 들면 회원기능 , 주문기능 , 재고기능 , 배송기능 등등의 모든
기능이 모두 합쳐진 하나의 프로젝트를 진행한다.
하지만 MSA 구조로 진행하게 되면 각각의 기능은 모두 분리되어 각각의 서비스로 배포되며 관리된다.
이렇게 진행을 하게 될 경우 장점은 하나의 서비스에 문제가 발생하여도 다른 기능들은 정상적으로 동작할수 있는 것이며 또 각각의 서비스를 별도로 관리하기 때문에 각각의 서비스만 필요한 업데이트가 있을경우 따로 배포를 진행할수 있고 서비스의 자원을 확장 하기에도 용이하다.
하지만 분리된 각 서비스로 인해 트랜잭션 처리에서 문제가 발생하게 된다.
그리고 각각 분리된 서비스를 하나의 서비스 처럼 동작하게 해야하기 때문에 초기 설정에서 조금더 비용이 필요하다.
MSA 를 해보지 않았어서 무언가 로망 ? 이 있어서 마냥 좋은거 라고만 생각했었는데 실제로 구현을 해보니까 완벽하게 구현하기란 매우 힘들고 부분적으로 많은 문제가 발생할수 있어서 어떤 경우에는 차라리 모놀리식 으로 구현하는게 더 좋을수도 있겠다 라는 생각이 들었다.
MSA 를 구현하면서 파악한 구조와 사용한 기술들에 대해서 서술하고자 한다.
가장 먼저 어떻게 서비스끼리 호출하는지를 알아야 했다.
세개의 각각 다른 서비스를 생성해서 포트를 설정하여 실행시키고 다른 하나의 서비스 에서 이 세가지 서비스를 호출해 보았다.
이때 각각의 서비스는 로컬에서는 지정한 포트와 IP 로 동작하기 때문에 문제가 없지만 서비스의 IP 와 포트는 일반적으로 동적으로 변경될수가 있다.
이때 서비스를 잘 찾아가기 위해서 Service Discovery 를 사용한다.
서비스 클라이언트가 Service register 에서 서비스 위치를 찾아서 호출하는 방식으로
비교적 구현이 간단하며 클라이언트가 사용 가능한 서비스 인스턴스에 대해 알고 있기 때문에 각 서비스별 로드 밸런싱 방법을 선택할수 있다.
하지만 클라이언트와 서비스 레지스트리가 연결되어 있어서 종속적이며 서비스 클라이언트에서 사용하는 각 언어 , 프레임워크 에 대해 클라이언트 측 서비스 검색 로직을 구현해야 한다.
호출되는 서비스 앞에 로드밸런서를 넣는 방식이고 클라이언트는 로드밸런서 를 호출하면 로드밸런서 가 Service Register 로 부터 등록된 서비스의 위치를 전달받는 방식으로
discovery 의 세부 사항이 클라이언트로부터 분리되어 있어서 의존성이 낮으며 분리되어 있어서 클라이언트는 단순히 로드밸런서에 요청만 한다.
하지만 로드밸런서가 배포환경에서 제공되어야 하며 제공되어 있지 않다면 설정 및 관리해야 하는 또다른 고 가용성 시스템이 요구된다.
하나의 서비스에서 다른 서비스를 사용할수 있음을 알았다.
여기서 생각해 볼 문제중 하나는 각각의 서비스가 따로 실행중일 경우 이 서비스들을 호출하기 전에 무언가 공통으로 처리할수 있는것들이 존재하지 않을까 였고
이 문제점을 효과적으로 해결할수 있는것이 API gateway 이다.
만약 API gateway 없이 각각 서비스를 호출하게 될 경우 클라이언트가 각각 서비스의 url 을 직접 호출해야 하며 인증/인가 , 로드 밸런싱 , 로깅 , 모니터링 등을 각각 관리해야 한다.
또 클라이언트에서 여러 API 를 호출해야 하는 경우 N+1 문제가 발생할수 있다.
기능
MSA 란 원래 하나의 서비스로 동작하던 것을 각각의 작은 단위의 서비스로 나눈것 이기 때문에 본래 하나이다. 따라서 하나의 입구 를 지정해서 이곳에서 관리한다면 조금더 효과적으로 전체 서비스들을 호출하고 관리할수 있다.
Spring Cloud Zuul 의 업데이트가 중단되면서 Spring Cloud Gateway 로 이전되었다.
비동기 방식을 통해 수많은 요청을 빠르게 처리할수 있으며 다른 Spring Cloud 기반 기술들과 통합이 잘 되어있다. (Spring 을 사용하지 않는다면 Nginx 를 사용했을 것이다.)
또 Service Discovery 와 함께 사용하면 gateway 를 통해 받은 요청에 대한 공통 처리를 해결하고 알맞은 서비스에 찾아가도록 할수 있다.
원래 하나의 구조에 속해있던 서비스들은 하나의 설정을 공유하고 있었다.
하지만 각각 서비스를 분리하게 되면서 각각 서비스에 필요한 설정들을 중복으로 적용해주어야 했다.
이때 한곳에서 모든 설정 정보를 관리하고 이를 호출하여 사용한다면 서비스를 관리하기 매우 편리해질 것이다.
장점
단점
** config server 를 사용하다가 후에 k8s 를 도입하여 변경하였다.
Config Server 를 통해서 설정 정보를 가져오게 되어있는데 만약 공통으로 관리하는 설정 정보가 변경되게 된다면 각 서비스는 변경된 값을 가져오기 위해 refresh 작업이 필요하다.
Spring Cloud Bus 란 메시지 큐를 사용하여 변경된 설정값을 서비스에 적용시켜주는 서비스이다.
일반적인 프로젝트의 설정은 application.yml or properties 에 적용한다.
하지만 yml 파일은 SpringBoot 시작 후에 로드된다. 따라서 bootstrap.yml 을 사용하면 SpringBoot 시작 전에 설정 정보를 로드할수 있다.
우선 로놀리식 구조로 모든 서비스를 만든다고 해보자.
회원 기능을 통해서 회원 가입을 완료했고 주문을 하려고 한다.
회원서비스를 통해 로그인을 하고 주문 서비스를 통해 주문을 해야한다.
이렇게 되면 회원 서비스 에서 주문 서비스를 호출해야 하는데 이 둘은 각각 다른 서비스 이기 때문에 서로 API 호출을 해야 한다.
제일 먼저 사용한 것은 Netflix 에서 개발한 Http Client 다.
서비스간 API 호출을 하는 방법은
대표적으로 세가지 이며 RESTful API 가 널리 사용되므로 구현이 비교적 간단하며
다양한 클라이언트와 연동 가능하며 JSON , XML 등의 데이터 형식을 지원한다.
하지만 HTTP 요청/응답 방식이라 레이턴시가 높고 실시간성이 떨어지며 높은 트래픽시 성능 저하 가능성이 있다.
위 두가지는 사용을 해봤어서 Spring Cloud 를 사용하기도 했었기에 FeignClient 를 사용해서 먼저 구현을 해보았다.
어노테이션을 사용해서 비교적 간단하게 구현을 할수 있었고 호출도 잘 되는것을 확인할수 있었다.
하지만 FeignClient 는 기본적으로 동기 방식이다.
동기방식의 호출은 트래픽이 몰리면 당연히 성능 저하가 발생하게 되고 문제가 발생할 경우 더 큰 성능저하가 발생할수 있다.
그래서 비동기로 처리할수 있는 메시지큐 방식의 Kafka 를 도입하게 되었다.
Kafka 란 Pub-Sub 모델의 메시지 큐 형태로 동작하며 분산환경에 특화된 통신 방법이다.
MSA 구조로 프로젝트를 진행할때 매우 많이 사용되고 있으며 비동기 방식이기 때문에 기존의 FeignClient 를 Kafka 로 변경하게 되었다.
또 MSA 란 각 서비스가 독립적 으로 분리가 되어 있어야 하는데 위의 방법들은 서비스 간의 의존성을 높이기 때문에 MSA 의 방향성과 맞지 않다고 생각했다.
카프카를 사용하게 되면 각 서비스 간의 결합 없이 서로를 호출할수 있다.
장점
위 장점들을 보면 MSA 구조 에서 필요한 대부분의 것들을 갖추고 있다.
따라서 MSA 구조를 택했다면 제일 먼저 Kafka 를 생각할것 같다.
하지만 카프카는 대규모 시스템을 위해 설계된 것으로 작은 서비스를 운영한다면 RabbitMQ 가 속도 면에서 더 빠르므로 서비스 규모를 생각해보고 사용해야 한다.
구성요소
위 사용법 외에도 데이터 동기화 작업에서 kafka 가 필요하게 되었는데
하나의 서비스 인스턴스가 여러개 존재하며 각각 DB 가 존재할때 이 서비스의 인스턴스들은 라운드로빈 방식으로 동작한다고 하면 서로 다른 DB 를 가지는 인스턴스들로 인해 같은 서비스 임에도 데이터 동기화가 이루어지지 않는다.
이때 카프카를 사용하여 한 서비스의 인스턴스 DB 들의 변동을 감지하여 동기화를 시켜줄수 있다.
MSA 는 각 서비스가 나누어져 있기 때문에 하나의 서비스에서 발생한 오류가 다른 서비스로 확장되지 않아야 한다.
이때 사용하는 것이 Circuit Breaker 로 Resilience4J 를 사용하여 실패율 , 대기시간 , 호출횟수 등을 설정하여 서비스간 통신에서 장애가 발생했을 경우 설정한 값들을 초과할 경우 더이상 호출하지 않고 다른 값으로 대체하여 처리할수 있게 해준다.
분리된 각 서비스들은 서로를 호출하여 요청을 처리하게 된다.
이때 어떤 요청이 어떤 서비스를 거쳐가는지에 대한 추적이 필요한데 Jaeger , Zipkin 과 같은 분산 트레이싱 툴을 사용하여 각 요청의 흐름을 파악할수 있었다.
SpringBoot 2 까지는 Spring Cloud Sleuth 를 사용했었지만 SpringBoot 3 부터는 Sleuth 는 더이상 지원하지 않아 Micrometer Tracing 으로 이전되었다고 한다.
각 분리된 서비스 로그들을 통합하여 확인할수 있는 시스템이 필요한데 ELK Stack 과 Prometheus 가 대표적이다.
Prometheus 를 사용하여 actuator 로 metrics 를 수집하고 이를 Grafana 로 시각화 하는 작업을 진행해 보았다.
(Istio 라는 좋은 기술을 알게 되었지만 아직 지식이 없어 사용해보진 못하였음)
MSA 의 큰 장점중 하나는 하나의 서비스에 변경이 일어난 경우 그 서비스만 다시 배포하면 된다는 것이다. 이를 위해 각 서비스는 자체적으로 빌드 , 테스트 , 배포 가 되는 파이프라인이 필요한데 git actions 와 travis 를 사용해 본적이 있다.
위 두가지 외에 argo 와 jenkins 에 대해 알아보았는데 k8s 를 사용한다면 네이티브한 argo 를 , 다양한 환경과 유연성을 요구하는 환경에서는 jenkins 를 선택할것 같다.