메뉴

문서정보

Monolithic에서 Microservice Architecture로

목차

소개

이 문서에서는 마이크로서비스 아키텍처를 디자인하는 방법을 여러 사례를 들어서 살펴볼 것이다. 먼저 일반적인 패턴을 소개하고 AWS에서 어떻게 구축할지를 살펴보도록 하겠다.

이 문서를 통해서 얻고자 하는 것은 다음과 같다. 이 문서에서 구축을 위한 상세 내용을 기술하기는 힘들 것 같아서 아이디어라고 범위를 한정 했다. 가능하다면 구현 및 구축을 위한 상세 내용들을 다루기 위해 노력은 할 것이다.

왜 아키텍처 인가

알고리즘을 직접 적으로 다루는 사람은 알고리즘이 중요하다고 한다. 나 ? 나는 문제를 해결 할 수 있는 코드를 만들면 된다는 관점이다. 이런 나의 관점은 알고리즘 시험으로 개발자를 뽑는게 큰 의미가 있나 ? 이다.

반면 나는 아키텍처링을 더 중요하게 생각한다. 클라우드 인프라의 설계 구축에 참여한 경험, 퍼블릭 클라우드에서 서비스를 개발한 경험일 때문일 것이다. 퍼블릭 클라우드의 경우 여기 저기 흩어진 상관 없어 보이는 기능, 기술, 서비스들을 마치 레고를 조립하는 것과 유사하기 때문이다. 레고에 설계도가 중요하듯이, 클라우드 환경에서의 애플리케이션 개발도 설계도가 중요 할 수 밖에 없다. 그래서 클라우드 서비스 제공자는 Best Priactices를 매우 중요하게 서비스 한다.

MSA가 클라우드(프라이빗이든 퍼블릭이든)의 발전과 함께 발전을 한 개념이다 보니, 아키텍처가 중요한 요소가 된다.

아키텍처의 수립시 고려 사항

아키텍처를 수립하기 위해서 아래의 질문에 대한 답이 있어야 한다. 이 질문에서 아키텍쳐가 고려해야 할 주요 항목이 나온다. 확장성과 가용성은 고객이 애플리케이션을 얼마나 잘 사용 할 수 있는지를 측정하는 기준이다. 이커머스 서비스가 가동 중단이 없이 수백만명의 사용자에게 서비스를 제공할 수 있다면 이 시스템은 높은 확장성과 가용성을 가지고 있다고 말할 수 있다. 높은 확장성과 가용성은 좋은 아키텍처를 설계하는 요소다. 이커머스 아키텍처를 수립해야 한다면 아래와 같은 아키텍처 요구사항을 수립 할 수 있을 것이다. 요구사항을 명확히하는 좋은 방법은 "수치화"하는 것이다. 위의 요구사항 중 확장성과 효율성 부분은 비교적 수치화하기가 수월하다. 동시접속자 수초당 요청, 지연을 수치화 해보자. 지연은 최대 수용가능한 시간이다.

동시접속 사용자 초당 요구 지연
2K 0.5K
20K 12K
100K 80K <=2 Sec
500K 300K ??
동시접속 사용자를 측정하려면 동시접속 사용자가 무언지를 정의해야 한다. 동시접속 사용자는 웹사이트가 한번에 처리 할 수 있는 동시 요청 수를 측정한다. 글 쓰기, 읽기, 좋아요 클릭, HTML 등 클라이언트에서 서버로 요청을 보내는 모든 활동을 의미한다.

일반적으로 동시접속 사용자는 성능측면에서 중요한 지표는 아니다. 초당 처리 할 수 있는 요청 수가 중요하다. 동시접속 사용자는 초당 요구를 산정하기 위한 기준정보로 사용한다.

초기에는 동시접속 사용자가 적을테니, 시스템을 구성하는데 문제가 없겠지만 사용자가 늘어나게 될 경우 심각한 문제가 될 수 있다. 그렇다고 처음부터 동시접속 사용자 500K를 수용 할 수 있는 거대 시스템을 만들 수는 없는 노릇이다. 우리는 작게 시작하면서도 유연하게 확장 할 수 있는 아키텍처를 만들어야 한다.

모놀리식 아키텍처

모놀리식 아키텍처는 수십년간 발전해 왔으며 고유의 장점과 단점을 가지고 있다.

우리가 가지고 있는 이커머스 서비스는 오래된 역사를 가지고 있으며, 당시 유일한 선택지였던 모놀리식 아키텍처로 개발했다. 우리는 이 시스템을 클라우드로 전환하며 이에 맞는 모델로 바꾸기로 했다. 정확하게는 클라우드 네이티브에서 MSA로 전환할 계획이다.

 모놀리식

모놀리식 애플리케이션이라면 대략 위의 모습으로 구현되어 있을 것이다. 모놀리식 애플리케이션은 클라이언트 인터페이스, 비즈니스 로직, 데이터베이스 모든 것이 하나의 코드베이스에서 관리 된다. 따라서 코드에 어떤 수정이 있을 경우, 코드 전체를 컴파일하고 테스트해서 배포한다.

모놀리식 애플리케이션의 주요한 장점은 단일 코드 기반이라서, 프로젝트를 관리하기 쉬우며 각 서비스들과 데이터베이스들이 유사한 패턴으로 강력하게 결합되어 있기 때문에 디버깅하기 쉽다는 장점이 있다.

이러한 장덤이 있지만 단점역시 있다. 모놀리식은 작은 규모의 소프트웨어를 개발 할 때 최고의 아키텍처이다. 모놀리식 소프트웨어는 간단하고 이해하기 쉽기 때문이다.

모놀리식 아키텍처

이제 모놀리식 아키텍처 애플리케이션을 단계별로 설계해 보자. 애플리케이션을 설계할 때 항상 그러하듯이 기능과 비기능을 정의 할 것이다.

기능 요구사항 비기능 요구사항 그리고 핵심 적인 아키텍처 원칙을 제시한다.

원칙 단순한 규칙이라고 생각할지 모르겠으나, 아키텍처를 수립하거나 소프트웨어를 개발 할때, 인프라를 설계하고 구축할 때, 이 원칙을 생각하고 행동하는 것만으로도 큰 도움이 될 것이다. "운동전 준비운동", "건너기전 좌우 확인" 이런 간단한 원칙만으로 얼마나 많은 사고를 줄일 수 있는지를 생각해보자.

이러한 기능/비기능 요구사항과 원칙에 따라서 아래와 같은 아키텍처가 만들어졌다.

 모노리딕 아키텍처

로드밸런서는 Nginx로 통합하고, E-Commerce 애플리케이션은 모든 기능이 통합된 단일한 war/jar로 배포한다. 사실상 하나의 프로세스가 데이터베이스에 접근하기 때문에, 데이터베이스 시스템도 Oracle, PostSQL, MySQL등의 RDBMS로 통합할 수 있다.

방대한 크기의 단일 서비스가 존재한다. 새로운 모듈을 동비하는 경우 기존 코드를 변경한 다음 완전히 다른 코드로 war 파일을 만들어서 배포해야 한다. 모노리식 아키텍처로 구현하더라도 KISS 원칙을 따를 수 있을 것이다. KISS 원칙에 따라서 요구사항을 식별하고 디자인을 리펙토링하고 이 과정을 단계적으로 반복한다.

클라이언트와의 접점에 NginX Load Balancer 를 두는 것으로 E-Commerce 애플리케이션을 수평 확장한다. 로드밸런서는 클라이언트의 요구사항을 균등하게 배포하는 것으로 모노리식 애플리케이션을 확장 할 수 있다.

마이크로서비스 아키텍처 설계

모놀리식 애플리케이션은 KISS 원칙에 따라 개발하고 있지만 시장의 요구에 빠르게 대응 할 수 있는 유연한 아키텍처로 전환하기로 했으며, MSA를 도입하기로 했다.

MSA 스타일에서는 각 서비스는 별개의 프로세스로 실행되며 이들은 HTTP, gRPC등을 통해서 서로 느슨하게 연결되는 것으로 애플리케이션을 구성한다. 라이브러리르 직접 호출하는 방식에서 네트워크를 이용한 호출로 변한 것이라고 볼 수도 있겠다. MsA에서 각 서비스는 아래의 특징을 가진다. 우리는 아래와 같은 MSA 애플리케이션을 설계했다. MSA를 명확히 이해하기 위해서, 몇 단계에 걸쳐서 설계를 완성해 나갈 것이다.

 MSA-1

Database per Services패턴을 따라서 모든 서비스는 각각의 데이터베이스를 가진다. 데이터베이스는 다른 서비스와 분리되기 때문에, 서비스에 맞는 데이터베이스를 사용 할 수 있다. RDBMS, NoSQL 어떤 것이든 사용 할 수 있다. 예를 들어Catalog는 RDBMS를 사용하고 Order는 DynamoDB를 이용 할 수 있을 것이다.

위의 아키텍처는 마이크로서비스를 식별하긴 했으나 클라이언트가 마이크로 서비스와 직접 통신을 하면서, 요청이 통합되어 관리되지 않는 문제가 있다. 클라이언트의 요청이 서비스에 전달되기 위해서는 "권한/인증", "라우팅 관리", "요청/응답검사", "보안"과 같은 사전/사후 단계를 거치게 된다. 각 서비스가 이러한 단계를 가지고 있는 것 보다는 이들 단계를 분리해서 클리아언트와 마이크로 서비스에 중간에 이들을 통합해서 관리 할 수 있는 계층을 둔다면 서비스의 복잡도를 위임할 수 있을 것이다.

이러한 문제를 해결하기 위해서 MSA는 API Gateway 패턴을 사용한다. API Gateway 패턴을 이용해서 지금의 아키텍처를 발전시켜 보자.

API Gateway 패턴 1차

API Gateway 패턴은 MSA에서는 거의 필수적으로 사용하고 있는 패턴이다. API-Gateway는 모든 앱에 대한 단일 진입점을 제공하며, 마이크로 서비스의 기능에 접근하기 위한 단일한 인터페이스를 제공한다. 또한 클라이언트와 마이크로 서비스 중간에서 요청과 응답을 중계하면서 라우팅, 프로토콜 변환, 프로토콜 검사, 속도 제한(Rate Limit), 요청 차단, 헤더추가 및 전파, 로깅, 보안감사, API 관리, 배포관리 등 공통 기능을 위임하기 위한 통일된 공간을 제공한다.

 MSA - 2

API Gateway가 단일 진입점이 되어서 요청을 수집하고 처리해서 내부의 마이크로 서비스에 전달하는 걸 확인 할 수 있다.

API Gateway 패턴 2차

API Gateway를 디바이스 별로 분리해서 운영하는 방법이 있다. 이렇게 할 경우 단일 실패지점을 제거 할 수 있다.

 MSA - 3

클라우드에서 제공하는 API Gateway(AWA API Gateway 같은)의 경우 플랫폼차원에서 가용성을 보장하는데 굳이 API-Gateway를 분리해서 운영 할 필요가 있는지는 고민해볼 필요가 있겠다. 클라우드 환경에서 구축한다면 쓸데없이 복잡도만 추가하는 것으로 보인다.

API Gateway 패턴 3차

이제 모든 요청은 API Gateway를 통해서 단일화 된다. 그러나 어떤 작업의 경우 클라이언트가 2개 이상의 마이크로 서비스를 방문해야 할 수 있다. 예를 들어서 Shopping cart에 있는 물건을 구매해야 한다고 가정해 보자. 아래와 같은 마이크로 서비스를 호출 할 것이다.
  1. 주문생성 요청
  2. 제품정보 요청
  3. 가격 계산
  4. 구매요청
  5. 빌링(Billing)
  6. 배송
 MSA - 4

이러한 서비스를 어떻게 통합 할 수 있을까. Shopping Cart가 받아서 처리하는 것은 당연히 좋은 방법이 아니다. 클라이언트가 순차적으로 6번의 작업을 처리하게 할 수 있지만 역시 좋은 방법이 아니다. 클라이언트는 6번의 HTTP 요청을 전송해야 하는데, 인터넷의 품질에 따라서 지연이 생길 수 있고 사용자에게 나쁜 경험을 주게 될 것이다. 3번의 API는 성공했는데, 4번째 실패하면 어떤 문제가 생길까. 인터넷은 우리가 제어 할 수 없기 때문에, 이러한 실패를 모니터링 하고 대응하는 것도 어려워진다. 이 문제를 해결 방법을 찾아보자.
  1. Service Aggregator 패턴을 사용하여, 하나의 API에서 특정 비즈니스 작업을 수행하도록 한다.
  2. 메시지브로커를 이용해서 비동기 방식으로 작동하도록 내부 메시징 시스템을 변경한다.
Service Aggregator 패턴을 이용해서 이 문제를 풀어보자.

 Service Aggreator 패턴

특정 작업을 모아서 처리하는 별도의 API를 만든다. 이런 API는 필요에 따라 늘어날 수 있다.

API Gateway 패턴 4차 - Message Queue

소수의 마이크로 서비스로 이루어진 시스템의 경우 동기식으로 처리하는게 좋다. 그러나 여러 마이크로 서비스로 구성되고, 서로를 호출하고 완료 할 때까지의 시간을 기다려야 한다면 비동기 통신을 고려해야 한다. 이런 구간을 비동기로 처리하지 않을 경우, 컴포넌트간 종속성이 증가한다. 즉 다른 구간으로 병목현상이 전파된다.

 Message Queue

여러 개의 상호작용하는 컴포넌트들을 느슨하게 연결하려면 MSA에 비동기 기반 통신을 사용 할 것을 고려해야 한다. 비동기 메시지 통신은 이벤트와 함께 작동하기 때문에, 이벤트 중심 커뮤니케이션 시스템이라고도 부른다.

게시/구독(PUB/SUB) 디자인 패턴 소개

PUB/SbU 시스템은 메시지의 발행자와 메시지 구독자를 두고 이들 사이에 메시지를 주고 받는 메시지 패턴이다.

 PUB/SUB

EventBus로 발행자와 구독자는 서로 분리된다. 발행자는 구독자가 아닌 메시지큐에 메시지를 전송하기만 하면된다. 구독자가 어떤 프로그램을 사용하든지 신경쓸 필요가 없다.

EventBus는 메시지 브로커(Message Broker)로 구성하면 된다. MSA를 구성하는 모든 컴포넌트들이 메시지 브로커를 바라보기 때문에, 시스템 구성상 백본의 역할을 하게 된다.

 Message Broker

PUB/SUB 메시지 브로커를 이용해서 Microservices Asynchronous Communications Design 패턴을 적용했다. 이 패턴을 위해서 사용 할 수 있는 2가지 기술 옵션이 있다.

비동기 응답

동기적인 시스템에서는 고객이 요청을하면 해당 요청으로 응답이 리턴된다. 예를들어 사용자가 물건을 주문하기 위해서 /order API를 호출하면, 이 요청의 응답으로 성공 혹은 실패가 리턴된다. 주문을 처리하는데 시간이 걸릴 경우 몇 초를 기다려야 할 수도 있다.

비동기적인 시스템은 요청을 받는 것과 요청을 처리하는 것이 분리된다.

 동기 vs 비동기

클라이언트가 요청을 보내면, 클라이언트는 작업 처리 결과를 받는 대신 "요청이 성공적으로 접수"돼었다는 접수처리 결과를 받는다. 이제 클라이언트는 작업처리 결과를 기다려야 한다. 기다리는 방법은 두 가지가 있다.
  1. 롱 폴링(Long Polling) : 작업결과를 알려주는 서버를 만든다. 클라이언트는 이 서버에 연결을 하는데, 일정시간(수초 이상)을 대기한다. 연결된 시간동안 서버는 데이터베이스 등에서 작업결과를 조회해서 작업이 완료되면 클라이언트에 응답한다.
  2. 웹 소켓(Web Socket) : TCP/IP 소켓 프로그램처럼 연결을 계속유지하면서 데이터를 주고 받는다. 작업이 완료되면 연결된 웹 소켓으로 데이터를 전송한다.
Order 서비스의 경우 아래와 같이 구성 할 수 있을 것이다.

 비동기 응답 - 1

Oder Service는 Order 처리 상태를 데이터베이스에 업데이트 할 것이다. Websocket Service가 주기적으로 데이터베이스를 읽어서 클라이언트에게 작업 결과를 실시간으로 전송한다. 심플한 방법이긴 한데, 데이터베이스를 주기적으로 조회해야 하는 문제가 있다. 동시에 1만명의 Order 요청이 있고, 1초 간격으로 데이터베이스를 확인한다면 초당 1만의 읽기 쿼리가 발생한다. NoSQL을 사용하고 클러스터에 투자를 한다면 처리 할 수는 있겠으나 확장성 측면에서 문제가 노출된다.

아래와 같이 시스템을 구성하면 좀 더 효율적일 것이다.

 비동기 응답 - 2

별도의 PUBSUB 채널을 만든다. Websocket 서비스는 사용자 ID를 Key로 PUBSUB 채널에 구독 신청을 한다. Order Service는 작업을 완료하면 데이터베이스를 업데이트하고 PUB/SUB 채널에 메시지를 게시한다. 이 메시지는 Websocket으로 클라이언트까지 전달된다.

AWS를 사용한다면 DynamoDB와 Lambda를 이용해서도 구성할 수 있다.

 DynamoDB를 이용한 구성

  1. 클라이언트는 웹소켓 서버에 연결한다.
  2. 클라이언트 연결정보를 DynamoDB에 저장한다.
  3. Order 상태가 업데이트되면
  4. DynamoDB Stream에 상태 업데이트 이벤트가 쌓이고
  5. DynamoDB Stream 이벤트로 부터 Lambda를 실행한다.
  6. Lambda는 DynamoDB에서 클라이언트의 연결정보를 읽는다.
  7. Lambda는 order Server API를 호출한다.
  8. Order Server는 웹소켓으로 Order 업데이트 정보를 전송한다.

마이크로 서비스에서의 데이터 관리

모놀리식은 단일 데이터베이스를 유지하기 때문에 여러 테이블을 가로지르는 쿼리를 만들기가 쉽다. 데이터에 대한 모든 변경사항은 함께 조회되고, 업데이트 된다. 관계형 데이터베이스는 ACID를 보장하기 때문에 데이터를 쉽게 관리하고 쿼리 할 수 있다.

마이크로 서비스에서는 각 서비스마다 모두 다른 데이터베이스를 가진다. 따라서 이 데이터베이스를 관리하는 전략을 설정해야 한다. 이 문서에서는 데이터베이스 관리 전략에 사용 할 수 있는 원칙과 몇 가지 패턴을 소개한다.

마이크로 서비스는 독립적이며 특정 기능만을 수행한다. 전자 상거래 응용 프로그램의 경우, 제품 카탈로그, 쇼핑 카트, 주문처리, 빌링 마이크로 서비스가 고객 사용 시나리오를 수행하기 위해서 서로 상호작용해야 한다. 따라서 마이크로 서비스는 서로 상호작용하며 통합되어야 한다. 이렇게 통합함으로써 집계를 위해서 각 서비스 데이터를 쿼리하고 비즈니스 로직을 수행 할 수 있다.

CQRS 디자인 패턴

기존 데이터베이스 아키텍처는 데이터베이스의 질의와 업데이트에 동일한 데이터 모델을 사용한다. 이러한 모델은 간단한 CRUD 작업에 적합하다. 그러나 더 복잡한 애플리케이션에서는 이러한 방법을 사용 하기 어려울 수 있다. 마이크로 서비스는 다양한 애플리케이션이 상호작용하기 때문에 복잡도를 높일 수 있다. CQRS 디자인 패턴은 비효율적인 join을 포함한 복잡한 쿼리를 피하기 위한 패턴으로 마이크로 서비스에서 유용하게 사용 할 수 있다.

CQRS(Command and Query Responsibility Segregation)은 명령과 조회의 책임/역할을 분리하는 패턴이다.

데이터베이스 모델로 널리 사용되는 CRUD의 경우, 비즈니스 로직의 대부분은 CUD(Create, Update, Delete)를 통해서 이루어진다. 읽기작업(Read)는 단순 데이터 조회가 대부분인데, 이들을 하나의 비즈니스 도메인에서 모두 처리하게 될경우 복잡도가 크게 늘어난다. 실제 데이터베이스 애플리케이션을 개발하다보면 CUD와 R이 서로 분리된다는 것을 경험적을 알고 있을 것이다. 하여 데이터처리(명령)와 조회를 분리하면 복잡도를 낮출 수 있을 것이라는 가설하에 CQRS가 만들어진다.

일반적인 CRUD 패턴은 아래와 같다.

 CRUD 패턴

CQRS 패턴은 아래와 같다. 조회와 명령의 분리를 묘사하고 있다.

 CQRS 패턴

MSA에서는 조회와 명령을 독립적인 서비스로 구축할 수 있다. 아래와 같은 모습이 될 것이다.

 MSA와 CQRS

command와 service가 독립적인 서비스로 분리된다. Command 서비스는 쓰기에, Query 서비스는 읽기에 최적화된 데이터베이스 모델, 데이터베이스 엔진을 사용 할 수 있다. 뷰 패턴은 읽기 데이터베이스를 구현하는 좋은 예다. 이렇게 하면, 쿼리 작업을 위해서 세분하된 테이블간의 복잡한 조인과 맵핑을 피할 수 있다.

Event Sourcing Pattern

CQRS는 이벤트 소싱 패턴과 함께 사용하는 경우가 많다. Command 모델에서 데이터를 쓰면, 이 이벤트로 부터 뷰 데이터를 생성하는 방식으로 작동하기 때문이다. CQRS와 이벤트소싱의 주요 사용 케이스는 쓰기 데이터베이스에 이벤트를 저장하는 것이다.

 Event Sourcing

이벤트 소싱 패턴은 최신의 데이터상태를 저장하는 대신 모든 이벤트를 순차적으로 데이터베이스에 저장한다. 이 저장소를 이벤트 저장소라고 한다. 즉 쇼핑카트에 아이템을 넣고 뺄 경우, 사용자 ID 등을 Key로하여 Insert, Delete, Update하는게 아니라 모든 이벤트를 Insert 한다. 따라서 이벤트 스토어에는 모든 트랜잭션 로그가 남게 된다.

이벤트 스토어는 이벤트를 이벤트 버스에 퍼블리싱하고, 이벤트 버스를 바라보고 있는 컨슈머가 이벤트를 읽어서 뷰 데이터베이스를 업데이트한다.

이 패턴은 애플리케이션에서 발행하는 모든 이벤트를 저장하기 때문에, 저장된 이벤트를 단순히 재처리하는 것 만으로 애플리케이션을 재구축 할 수 있다. 원시로그나 마찬가지이기 때문이다.

 이벤트 소싱 패턴

AWS의 경우 Amazon Kinesis, Amazon SQS, Amazon MQ, Amazon MSK 등을 이용해서 이벤트 소싱 패턴을 구현 할 수 있다. 예를 들어 Amazon Kinesis의 경우 이벤트는 스트림 레코드로 기록되며, AWS Lambda로 이벤트를 검색해서 처리 할 수 있다.

 AWS 이벤트 소싱 패턴 구현

워크플로우는 아래와 같이 구성된다.
  1. "/withdraw" 와 "/credit" 마이크로 서비스가 호출되면 상태가 변경된다. 이 정보를 Kinesis Data Streams에 게시한다.
  2. Kinesis Data Streams에 이벤트가 게시되면 Lambda 함수가 호출된다. 이 함수는 이벤트를 읽어서 Query microservice가 사용할 수 있는 데이터를 생성한다. /withdraw 결과로 "잔액(balance)", /credit 결과로 "신용한도(credit limit)"가 생성된다.
  3. 사용자가 /balance, /creditLimit API를 호출하면, Query microservice로 요청이 전달되서 처리된다.

API Gateway, CQRS, Event Sourcing, Eventual Consistency 패턴 통합

지금까지 소개했던 패턴들을 통합해서 아키텍처를 만들었다.

 MSA 통합 패턴

API Gateway 패턴을 이용해서 단일 서비스 지점을 만든다. 각 기능들은 독립적인 서비스로 서로 격리된다. Order 서비스의 경우에는 CQRS 패턴과 Event Sourcing 패턴을 사용해서 읽기와 쓰기를 분리한다. 분리된 읽기와 쓰기는 Message Broker를 통해서 동기화 된다. 읽기와 쓰기가 분리되므로 최적화된 데이터베이스를 선택 할 수 있다. 아래와 같은 기술셋을 검토 할 수 있을 것이다.

AWS에서는 대략 아래와 같이 구성한다.

 AWS

참고