안녕하세요 :)
이제부터 어떻게 Monolithic 아키텍처에서 MSA 아키텍처로 진화했는지 한번 알아보겠습니다.
오늘 나오는 주요 토픽은 아래와 같습니다.
- Monolithic부터 MSA (Microservice Architecture)가 생기는 과정
- Event Driven Architecture
- Saga Pattern
- CQRS
- API Gateway
- Circuit Breaker Pattern
- Orchestration
여기서 나오는 이야기의 순서는 실제 역사의 진행 순서는 아니라는 거를 알고 봐주시면 좋을 것 같습니다.
Monolithic Architecture
태초에는 당연히 모놀리식 아키텍처가 있었죠. 하나의 코드 베이스가 하나의 실행 환경에서 함께 동작하는 구조에요
배포 단위가 하나이기 때문에 확장을 하려면은 전체를 한 번에 스케일링을 해줘야 됐었죠. 그게 Horizontal 스케일링이 됐든 Vertical 스케일링이 됐든요
자 이렇게 개발하면 사이클이 느려지니까 배포단위를 나누자는 의견으로 처음에 시작을 했어요.
Single Responsibility Principle
그래서 도메인별로 Single Responsibility Principle을 지키면서 서비스를 나누기 시작했고 이게 마이크로 서비스의 시초가 됩니다.
단일 데이터베이스의 문제
여기서 마이크로서비스를 나누어 운영하다 보니까 데이터베이스가 통합되어 있는게 또 문제가 됩니다. 각 서비스의 코드는 독립적으로 작업하고 독립적으로 배포하지만 데이터베이스는 통합적으로 관리해야 되기 때문에 동기화 문제가 생기고 결국 또 서비스끼리 커플링되는 문제가 있었어요
독립적인 Persistence Layer
그래서 데이터베이스를 아예 마이크로서비스별로 나눠 버리게 됐고 각 마이크로서비스 별로 독립적인 퍼시스턴스 레이어를 가질 수 있도록 설계를 했어요 이런 형태가 바로 우리가 현대에 가장 많이 사용하는 마이크로서비스의 구조라고 볼 수가 있습니다.
하지만 퍼시스턴스 레이어까지 나누니까 한 가지 문제가 또 생겨요
복잡한 통신의 문제
마이크로서비스끼리 통신을 더욱더 많이 해야되고 더욱 더 자주 해야된다는거죠. 우리가 흔히 사용하는 HTTP를 이용해서 통신을 하기에는
레이턴시로 인한 손실이랑 자유도로 인한 연동 난이도가 굉장한 문제가 됐어요
gRPC
그래서 더욱더 로우 레벨에서 작동하는 gRPC 같은 Remote Procedure Call 이라고 하죠. 우리가 흔히 RPC라고 부르는 기술을 사용하기 시작하게 됐습니다. RPC는 일반적으로 바이너리 포멧을 사용하고 최소한의 메타 데이터와 압축된 요청 응답을 사용하기 때문에 속도에서 큰 강점을 보입니다. RPC를 사용해서 더욱 빠른 통신을 하게되니까 레이턴시로 인한 손실은 줄었는데 서비스끼리 또 타이트하게 커플링되는 문제가 생깁니다.
Synchronous Networking
A 서비스가 B 서비스에 요청을 보내면 B 서비스가 응답을 보낼 때까지 A 서비스는 기다려야 되고 만약에 B 서비스가 C 서비스 D 서비스에 또 요청을 해야하고 그 서비스들이 또 다른 서비스들에 요청을 해서 응답을 만들어내야 한다면 엄청난 연쇄적인 워터폴 이펙트가 생기죠 이런 응답 요청 방식을 동기 방식이라고 얘기를 합니다.
Event Driven Architecture
이런 워터풀 이펙트를 없애기 위해 Asynchronous 방식들을 개발하게 돼요. 그중에 가장 인기있는 아키텍처 하나가 Event Driven Architecture 입니다. 요청을 보내면 응답을 받아야하는 기존 방식과는 다르게 Event Driven Architecture는 이벤트를 생성하고서 잊어버리면 됩니다. 그럼 특정 이벤트를 구독하고 있는 다른 마이크로서비스들이 이벤트를 전달받고 이벤트 생성자인 프로듀서에 독립적으로 이벤트를 구독하고 있는 컨슈머들이 이벤트를 프로세싱할 수가 있어요 그래서 즉각적인 응답이 필요한 경우에는 동기 통신을 여전히 사용할 수 밖에 없기는 하겠지만 비동기 통신을 사용해서 우리가 네트워킹 오버헤드를 최적화를 할 수 가 있게되는거죠
여기서 통신 방법을 최적화하고 나니까 어쩔수 없는 또 다른 문제가 생깁니다. 아무리 통신이 빨라도 마이크로서비스 간의 데이터를 동기화 하고 로직의 흐름을 하나로 묶어야 하는 어쩔 수 없는 니즈들이 생기기 시작했어요.
Saga Pattern
예를 들어서 결제를 진행한다고 가정을 하면 Product Service에 제고를 줄이고 Payment Service에서 결제를 진행한 다음에 User Service에서 포인트를 추가하고 Notification Service에서 알림을 보내줘야 돼요
모든 이벤트가 문제 없이 진행이 되면은 상관없겠지만 만약 어디선가 문제가 생긴다고 우리가 가정을 한다면 롤백을 해줘야합니다.
우리가 데이터베이스를 하나를 사용하면 트랜잭션으로 이거를 해결할 수가 있는데 이제는 분산된 데이터베이스들을 사용하기 때문에 이걸 해결하기 위해 Saga Pattern이라는걸 발명하게 됩니다. 여러 마이크로 서비스의 작업을 트랜잭션으로 묶어 주고 문제가 생기면은 문제를 해결하는 보상 트랜잭션으로 롤백을 해주는거죠 Saga Pattern은 트랜잭션을 중계하는 중계자가 하나 필요하기 때문에 Event Driven Architecture랑 사용을 하면 특히나 유용합니다. 이 정도 되면은 마이크로서비스가 매우 최적화 됐다고 생각할 수 있겠지만 개발자들의 욕심은 끝이 없었습니다. 기존 모놀리식 아키텍처에서는 CRUD 로 작업을 나누느게 가장 합리적이였는데 마이크로서비스를 운영을 해보다 보니 마이크서비스별로 담당하게 되는 오퍼레이션이 확연이 나뉜다는겁니다.
CQRS
어떤 마이크로 서비스는 데이터를 생성하는 역할만 주로 하게 되고 어떤 마이크로서비스는 데이터를 조회하는 역할을 주로 하게 된다는 겁니다. 그래서 마이크로서비스를 생성하는 역할인 커맨드와 조회하는 역할인 쿼리로 나눠지게 되는데요 이렇게 되면 각각 마이크로 서비스 별로 생성에 최적화된 데이터베이스를 사용하거나 조회에 최적화된 데이터베이스를 사용할 수가 있고 데이터 구조도 역할에 최적화해서 구현할 수 있기 때문에 더욱 더 효율이 높아지게 되는거죠. 게다가 각각 독립적으로 스케일링이 가능해지니까 인프라 최적화도 이제는 더욱 더 타이트하게 가능해졌어요 이걸 저희가 CQRS Command Query Responsibility Segregation 이라고 커맨드와 쿼리의 역할을 나누는다는 의미로 우리가 CQRS 패턴이라고 부르게 됩니다.
이렇게 마이크로서비스를 수도 없이 만들다 보니까 백엔드 구조가 너무 복잡해집니다. 그래서 직접 각각 마이크로서비스에 통신을 하는거는 사실상 불가능해집니다.
API Gateway
그래서 백엔드로 통신을 할 수 있게 해주는 하나의 관문인 API Gateway 라는 걸 만들게 됩니다. 모든 클라이언트의 요청은 API Gateway를 통하게 되고 API Gateway에서 알아서 적합한 마이크로 서비스에 요청을 전달해주고 응답을 만들어서 반환을 해주는거죠
Circuit Breaker Pattern
아키텍처가 복잡해지다 보니까 또 다른 문제가 생겼습니다. 하나의 마이크로서비스가 다운이 되면 연쇄적으로 연관 마이크로서비스들이 다운될 수가 있다는거에요 하나의 작은 문제가 전체 시스템을 다운시키는 헬파티를 불러올 수가 있겠죠...
그래서 어떤 한 마이크로서비스의 문제가 발생했을 때 연쇄적으로 문제가 전파되지 않도록 차단하는게 바로 Circuit Breaker Pattern 입니다. 이쯤되면 사람 개개인이 마이크로서비스들을 모두 운영하는 거는 사실상 말도 안 되는 상황이 옵니다.
Orchestration
그렇기 때문에 우리는 Kubernetes 같은 오케스트레이션을 사용하게 됩니다. 여기서 오케스트레이션이라하면 모든 서비스들이 함께 잘 융화되고 건강하게 잘 실행이 될 수 있도록 지휘하는 역할을 하는게 바로 오케스트레이션입니다.
현대 마이크로서비스를 바라봤을 때 어떤 컴포넌트와 패턴들이 사용이 되는지 간단하게 알아봤는데요
이 외에도 마이크로서비스를 사용했을 때 고려해야 될 점들이 매우 많을겁니다. 앞으로는 이 고려해야할 점을 하나씩 살펴보도록 하겠습니다.
감사합니다 :)