카테고리 없음

MSA, EDA 그리고 Event Sourcing 의 이해

Cloud Applicaiton Architect 2022. 6. 7. 13:12
반응형

마이크로서비스 아키텍처

마이크로서비스(Microservice)란, 하나의 큰 애플리케이션을 여러 개의 다른 역할을 수행하는 애플리케이션으로 분리하였을 때 각 애플리케이션을 의미하며, 이렇게 마이크로서비스를 분리하여 여러 개의 작은 애플리케이션으로 쪼개어 변경과 조합이 가능하도록 만든 아키텍처를 마이크로서비스 아키텍처라고 말합니다. 애플리케이션을 특화된 기능별로 나누게 되면 자연스럽게 애플리케이션의 추상화(abstraction)가 가능해집니다. 다시 말해, ‘인증’을 담당하는 서비스(예, auth.example.com)는 그 구체적인 구현 내용을 모르더라도 다른 서비스에서 약속된 인터페이스를 이용해 인증 과정을 수행할 수 있습니다. 또한, 검색창의 ‘자동완성’을 담당하는 서비스(예, autocomplete.example.com)는 사용자의 입력을 받아서 자동완성 결과만을 응답해주면 되기 때문에 해당 API를 유지한 상태에서 세부적인 구현내용을 언제든지 손쉽게 개선하고 변경할 수 있습니다.

모든 기술의 발전이 다 그렇듯이 마이크로서비스 역시 기존에 없다가 갑자기 등장한 개념은 아닙니다. 많은 기업들에서는 이미 이와 같은 방식으로 서비스를 분리하여 애플리케이션을 만들었으며 그때 그때 다양한 용어로 이름 붙여져 왔습니다. 그러나 최근 들어, REST API의 일반화, 도커(Docker)와 같은 컨테이너 기술, 클라우드 컴퓨팅 환경의 발전 등에 힘입어 마이크로서비스는 좀 더 손쉽게 구현될 수 있게 되었습니다.

마이크로서비스 아키텍처는 언제 필요한가?

모든 애플리케이션이 마이크로서비스 아키텍처 패턴으로 구성될 필요는 없습니다. 특히, 적은 인원으로 빠르게 시작해야하는 스타트업의 경우 앞으로 어떤 서비스와 컴포넌트가 필요하게 될 지 예측할 수 없는 상황에서 과도하게 시스템을 여러개의 서비스로 쪼갤 필요는 없습니다. (물론, 서비스에 대한 로드맵이 명확하여 초기에 시스템을 마이크로서비스 형태로 구성하는 것도 가능합니다). 그렇다면, 구체적으로 어느 시점에 마이크로서비스 아키텍처에 대해서 고려하는 것이 좋을까요? 일반적으로 다음의 항목들 중에서 대부분이 현재 상황에 해당한다고 생각되면 마이크로서비스 아키텍처 패턴에 대해서 고민을 시작해 보는 것도 나쁘지 않습니다.

  1. 애플리케이션의 배포에 한 시간 이상 소요된다.
  2. 단순한 기능 하나를 수정해도 전체 기능에 대한 QA가 필요하다.
  3. 단순한 버그 수정이 더 중대한 버그를 생산하는 일이 많아졌다.
  4. 현재의 애플리케이션을 기능별로 나눈다고 가정했을 때 수십개의 마이크로서비스가 가능하다.

모놀리틱 아키텍처 (MONOLITHIC ARCHITECTURE)

마이크로서비스 아키텍처를 잘 이해하기 위해서는 먼저, 반대되는 개념인 모놀리틱(Monolithic) 아키텍처에 대해서 살펴볼 필요가 있습니다. 이해를 돕기 위해서, 아마존(Amazon.com)과 유사한 온라인 쇼핑몰을 만든다고 가정해 보겠습니다. 기본적으로 처음 설계되는 애플리케이션의 구조는 그림 1과 같이 비즈니스 로직을 담당하고 있는 애플리케이션이 존재하고, 해당 애플리케이션은 데이터베이스 등 외부 시스템과 특정 프로토콜로 통신을 하게 됩니다. 또한, 사용자에게 인터페이스를 제공하기 위해서 HTML을 렌더링하는 부분과 RESTful API를 제공하는 부분을 갖게 됩니다. 이렇게 구성된 애플리케이션의 소스코드는 하나의 프로젝트로 구성되어 있으며 단일한 패키지로 배포되게 됩니다.

 

그림 1. 모놀리틱 아키텍처

이러한 구성의 애플리케이션은 특별히 이상할 것도 없고 실제로 많은 서비스들이 이와 같은 구성으로 이루어져 있습니다. 이렇게 단순한 구성의 애플리케이션은 로컬 환경에서 개발하기에도 편리하고 통합 시나리오 테스트를 수행하기에도 가장 손쉬운 구성입니다. 또한, 모든 코드가 하나의 묶음으로 구성되어 있기 때문에 배포도 매우 간편해집니다.

하지만 이러한 단순한 애플리케이션의 아키텍처는 서비스가 지속적으로 성장하고 규모가 커질 때 한계에 부딪히게 됩니다. 예를 들어, 3명의 개발자가 몇 가지 핵심 기능을 개발할 때에는 이와 같은 모놀리틱 아키텍처가 최적의 효율성을 보장하지만 개발자의 규모가 수십에서 백명 이상이 되고 서비스의 복잡도가 증가되면 아주 간단한 기능을 하나 추가하기 위해서도 매우 많은 줄의 코드를 수정해야함은 물론, 코드의 변화가 영향을 미치는 범위가 증가되었기 때문에 간단한 변화 하나에도 통합 테스트가 필요하게 됩니다.

많은 회사에서는 이러한 문제를 해결하기 위해서 여러가지 프로세스를 도입하고 애자일 철학을 기반으로 둔 여러가지 방법론을 적용해보고자 노력합니다. 하지만 실제로 이러한 시도가 서비스 구조의 근본적인 원인을 해결하지는 못하기 때문에 좋은 성과를 거두지 못하게 됩니다. 대부분의 경우 근본적인 원인은 서비스의 구조 자체가 너무 복잡하다는 점입니다. 복잡한 구조는 서비스 초창기 부터 함께 개발을 하여 전체 히스토리를 알고 있는 소수의 개발자를 제외하고는 대부분의 개발자들이 전체적인 시스템의 구조를 알지 못하기 때문에 재활용 가능한 모듈을 무시하고 중복된 코드를 생산하게 되며 사용하지 않는 코드가 기술 부채로 계속 쌓이게 됩니다. 또한, 코드가 서로 다양한 방식으로 연관되어 있기 때문에 간단한 버그 수정이 더 큰 버그를 양산하게 되는 결과를 초래합니다.

서비스 복잡도가 증가하면서 모놀리틱 아키텍처가 가지는 문제점들은 배포 시간의 증가, 부분적 스케일 아웃의 어려움, 안정성의 감소 등 여러가지가 있습니다. 그 중에서도 굳이 한가지를 꼽자면 애플리케이션을 구성하는 프로그래밍 언어, 또는 프레임워크의 변경이 거의 불가능에 가까울 정도로 어렵다는 점 입니다. 예를 들어, 애플리케이션에서 사용자의 인증만을 담당하는 요소가 별도의 서비스로 구현되어 있다면 필요에 따라 성능과 안정성을 증가시킬 수 있는 새로운 프레임워크로 변경하는 것을 고려해볼 수 있습니다. 하지만 만약 전체 애플리케이션이 하나로 묶여 있다면 그 동안 개발된 방대한 양의 코드를 새로운 언어, 또는 프레임워크로 전환해야 하기 때문에 대부분 시도조차 할 수 없을 것 입니다.

마이크로서비스 아키텍처 (MICROSERVICE ARCHITECTURE)

마이크로서비스 아키텍처 패턴은 그 이름에서도 유추할 수 있듯이 모놀리틱 아키텍처로 구성된 하나의 큰 서비스를 독립적인 역할을 수행하는 작은 단위의 서비스로 분리하여 설계하는 패턴을 말합니다. 여기에서 말하는 독립적인 역할이란 주로, ‘사용자 관리’, ‘주문 관리’, ‘결제 관리’, ‘알림 관리’와 같이 기능적인 요소를 의미합니다. 각각의 마이크로서비스는 그 크기만 작을 뿐, 자세히 살펴보면 각각이 하나의 모놀리틱 아키텍처와 유사한 구조를 갖습니다. 다만, 하나의 서비스에서 처리해야 하는 기능과 규모가 작기 때문에 이를 마이크로서비스라고 부릅니다.

 

그림 2 마이크로서비스 아키텍처

그림 2는 마이크로서비스 아키텍처의 개략적인 모습을 몇 가지 예시로 나타낸 그림이며 이 예시는 총 4개의 마이크로 서비스로 구성되어 있습니다. 사용자 서비스는 REST API를 이용해서 주문 서비스를 활용하며 API Gateway를 통해 정보를 웹브라우저 화면에 표시하거나 모바일 클라이언트에 데이터를 제공합니다. 또한, 사용자에게 알림이 필요한 경우 실제 알림이 어떤 과정을 통해서 처리되는지 신경쓸 필요없이 알림 서비스(Notification service)를 이용하여 원하는 요청을 호출할 수 있습니다. 그림에 표시된 API Gateway는 뒷쪽에서 자세히 설명하도록 하겠습니다.

마이크로서비스 아키텍처를 나타낸 그림에서 주목할 점은 사용자를 위한 데이터베이스와, 주문을 위한 데이터베이스가 따로 표시되어 있다는 점입니다. 이 부분은 실제로 마이크로서비스 아키텍처를 구현할 때 매우 중요한 부분인데, 전통적인 모놀리틱 아키텍처에서 주로 개발을 했던 경험에 비추어 보면 데이터의 트랜잭션 관리나 정규화 등의 관점에서 매우 비효율적으로 보일 수 있습니다. 물론, 하나의 데이터베이스를 각각의 개별 서비스가 공유해서 사용하는 방식도 가능하지만 마이크로서비스 아키텍처가 가지는 근본적인 장점을 최대한 활용하기 위해서는 이렇게 서비스별로 별도의 데이터베이스를 사용하는 것이 필요합니다. 또한, 데이터베이스(DBMS)의 종류 자체도 반드시 한가지 통일할 필요 없이 데이터의 특성과 서비스가 가지는 특수성에 따라 가장 효율적인 데이터베이스를 선택하여 사용하는 것도 가능합니다. 예를 들어, 어떤 서비스에서 사용하는 데이터는 변경이 적고 주로 읽기(read) 작업만 수행되는 반면, 또 다른 서비스의 데이터는 읽기 작업보다 빠른 속도로 쓰여지는(write) 작업이 대부분이라면 각각의 서비스 특성에 맞게 데이터베이스의 종류를 결정하고 설계할 수 있습니다.

마이크로서비스 아키텍처의 장점

마이크로서비스 아키텍처는 서비스의 규모가 커지고 복잡도가 증가할 수록 여러가지 장점을 갖습니다. 우선 서비스가 개별적으로 독립적인 단위의 애플리케이션이기 때문에 변경이 용이하고 그 변경이 다른 서비스에 미치는 영향이 적습니다. 또한, 개별 서비스 단위의 배포가 가능하기 때문에 하루에도 필요에 따라 여러 번 배포를 하는 것이 가능합니다. 비용적인 측면에서 보자면 마이크로서비스 아키텍처는 부하가 집중되는 특정 서비스를 위해 전체 애플리케이션을 스케일 아웃할 필요가 없기 때문에 불필요한 자원의 낭비를 줄일 수 있습니다. 특히, 서비스의 특성에 따라서 메모리 사용이 많은 서비스도 있을 수 있고, 계산 과정이 많아서 CPU 사용량이 많은 서비스가 있을 경우 서비스의 특성에 맞게 자원을 할당하여 스케일 아웃할 수 있기 때문에 효율적인 자원사용이 가능하게 됩니다.

이 외에도 마이크로서비스 아키텍처는 다양한 장점을 가지고 있지만, 여기에서 가장 강조하고 싶은 장점 중 하나는 시스템의 아키텍처가 개발 조직과 나아가서 회사의 조직 문화에 큰 영향을 미친다는 점입니다. 웹 서비스를 기반으로 하는 대부분의 회사들은 애자일의 사상을 도입하고 여러가지 방법론들을 채택하여 빠르고 유연한 개발 문화를 만들고자 노력합니다. 하지만 서비스의 규모가 커지고 시스템이 복잡해지면 사소한 변경 하나가 발생시킬 수 있는 문제(side effect)가 많아지고, 이 때문에 조직은 복잡한 시스템에 맞는 복잡한 프로세스를 필연적으로 가지게 됩니다. 마이크로서비스 아키텍처의 가장 큰 장점은 특정 서비스의 변경이 다른 서비스에 영향을 미칠 가능성이 적다는 점과 서비스 단위로 독립적인 배포가 가능하다는 점 입니다. 다시 말해, 인증과 관련된 서비스가 독립적으로 분리되어 있다면 해당 서비스의 개선과 수정 작업이 다른 서비스의 이해 당사자들과 독립적으로 진행될 수 있기 때문에 의사결정이 빠르고, 독립적인 테스트의 구축이 용이하기 때문에 품질이 증가하게 됩니다 (다른 서비스와 연계된 통합 테스트는 서비스가 분리될 수록 오히려 복잡도가 높아지게 됩니다). 이것은 다시 말해, 조직의 의사결정 프로세스와 테스트 및 배포 프로세스 등 많은 부분에 영향을 미친다는 것을 의미합니다.

마이크로서비스 아키텍처의 단점

반면, 마이크로서비스 아키텍처가 모든 면에서 장점만을 갖는 것은 아닙니다. 마이크로서비스 아키텍처가 가지는 대표적인 단점으로는 모놀리틱 아키텍처에 비해 서비스 간의 통신에 대한 처리가 추가적으로 필요하다는 점입니다. 이것은 단순히 개발해야 하는 코드의 양이 늘어난다는 점 뿐만 아니라, 사용자의 요청을 처리하기 위한 응답속도의 증가에도 영향을 미칩니다. 뿐만 아니라, 분산된 데이터베이스는 트랜잭션 관리가 용이하지 않기 때문에 데이터의 정합성을 맞추기 위한 노력이 추가적으로 필요합니다. 대부분의 모놀리틱 아키텍처에서는 하나의 데이터베이스를 사용하기 때문에 트랜잭션에 대한 처리가 크게 어렵지 않습니다. 하지만 서로다른 데이터베이스, 심지어 종류도 서로 다른 데이터베이스 내의 데이터의 정합성을 유지하기 위한 트랜잭션 처리는 대부분의 데이터베이스가 자체적으로 지원하지 않기 때문에 애플리케이션의 개발과정에서 항상 고려해야 한다는 어려움이 있습니다.

앞서 설명한 바와 같이 마이크로서비스 아키텍처는 서비스의 특성에 따라 효율적인 자원 사용이 가능하도록 스케일 아웃이 가능한 장점이 있는데 반해, 수 많은 서비스를 배포하고 관리하는 데에 어려움이 있습니다. 넷플릭스(Netflix)와 같은 대규모 서비스들은 보통 수백개 이상의 마이크로서비스로 이루어지는데 이렇게 많은 서비스들은 각각 서로 분산되어 있기 때문에 관리 포인트가 증가하고 통합해서 모니터링하고 운영하는 것이 모놀리틱 아키텍처에 비해 매우 어려워집니다. 이것은 필연적으로 매우 정교한 배포 자동화를 필요로 하며 많은 PaaS(Platform as a Service) 서비스, 또는 도커(Docker)와 같은 컨테이너 기술을 활용하여 도움을 받을 수 있습니다.


API Gateway

그림 3은 우리가 많이 사용하는 검색 포털 사이트의 메인 화면입니다. 사용자는 메인 페이지를 보기 위해서 하나의 URL을 통해 서버에 요청을 보내지만 실제 사용자가 보게 되는 화면에는 다양한 종류의 서비스 결과들 입니다. 그림에서 보여지는 붉은색 상자는 개별 서비스를 나타내며 위에서 부터 순서대로 이름을 붙여보자면, ‘사용자 서비스’, ‘뉴스 서비스’, ‘실시간 급상승 검색어 서비스’, ‘광고 서비스’, ‘날씨 서비스’ 등으로 나눌 수 있습니다.

 

그림 3. 검색 포털 메인화면에서의 다양한 종류의 서비스

만약, 모놀리틱 아키텍처를 이용해서 이와 같은 요청을 처리하는 애플리케이션을 구현하였다면 실제 요청이 도착하여 결과를 반환하기 까지 서버는 다양한 종류의 쿼리를 데이터베이스에 보내게 됩니다. 

 

그림 4. 모놀리틱 아키텍처에서의 요청 처리

이러한 방식은 애플리케이션 아키텍처가 간단하다는 장점은 있지만 특정 서비스에 변경이 있을 경우 이 서비스를 포함하고 있는 모든 코드를 찾아서 수정해 주어야만 하는 어려움이 있습니다.

그렇다면, 마이크로서비스 아키텍처를 갖는 애플리케이션에서는 이와 같은 요청에 대한 처리를 어떻게 수행하게 될까요? 그럼 5는 각각의 마이크로서비스들이 요청에서 필요한 각 영역을 담당하여 처리하는 모습을 보여줍니다.

그림 5. 마이크로서비스 아키텍처에서의 요청 처리

그림 5에서 볼 수 있듯이 각각의 마이크로서비스들은 각각이 담당한 내용에 대한 응답을 클라이언트에 보내주어서 임무를 완수합니다. 하지만, 이론적으로는 가능할지 몰라도 이를 실제로 구현하다 보면 몇 가지 어려움이 있습니다.

먼저, 클라이언트(Web UI 또는 모바일 앱)는 메인 페이지를 화면에 표시하기 위해서 연관된 모든 마이크로서비스에 각각 요청을 보내야만 합니다. 우리가 살펴보는 예제에서는 총 5개의 마이크로서비스가 관련되어 있지만 실제로는 이 보다 훨씬 더 많은 요청을 호출해야하는 경우도 있을 수 있습니다. 각각의 마이크로서비스에 요청을 보내기 위해서는 클라이언트가 모든 마이크로서비스의 호스트명은 물론 end_point를 알고 있어야 합니다. 즉, 마이크로서비스가 추가되거나 호스트정보가 변경되면 클라이언트가 가지고 있는 정보 역시 함께 수정해 주어야 합니다. 뿐만 아니라, 클라이언트는 서버에 요청을 보내고 응답을 받기 위해서 네트워크 지연속도(latency)가 필요한데, 요청의 회수가 증가할 수록 이 지연속도는 선형적으로 증가할 수 밖에서 없는 문제를 가지고 있습니다. 즉, 요청을 보내야하는 서비스의 개수가 증가할 수록 응답속도가 늦어진다는 점입니다.

두 번째로, 이와 같은 방식의 요청처리를 클라이언트에서 구현하려면 코드가 매우 복잡해지게 됩니다. 일반적으로 클라이언트는 요청을 보내야하는 서버의 호스트를 명시한 뒤 각각의 상황에 맞게 end_point를 변경하며 요청을 보냅니다. 그러나 이와 같이 여러개의 마이크로서비스에 요청을 보내기 위해서는 모든 마이크로서비스의 주소를 저장해 두어야하며, 요청을 보낼 때마다 해당 요청이 어떤 서비스에 보내는 것인지를 명시해 주어야 하기 때문에 필연적으로 소스코드가 복잡해지는 결과를 초래합니다.

세 번째로, 모든 마이크로서비스가 웹 통신에 적합한 프로토콜로 통신하지는 않는다는 점입니다. 많은 마이크로서비스들은 사용자에게 주는 기능적인 관점에서 나누어져 있기 때문에 HTTP 통신을 제공하지만 일부 서비스들은 서비스 자체가 가지는 특성에 따라 더 알맞는 프로토콜을 사용할 수 있습니다. 예를 들어, 사용자에게 알림을 보내주기 위한 서비스의 경우 요청을 순차적으로 빠르게 처리하기 위해서 메시지 큐와 관련된 프로토콜을 사용할 수도 있습니다. 이러한 방식의 프로토콜들은 모바일 앱 또는 웹 브라우저가 직접 통신하기에는 적합하지 않을 뿐만 아니라, 보안상으로도 방화벽(firewall) 내부에 위치하면서 외부에서 직접 접근하는 것을 차단해야만 합니다.

마지막으로, 이러한 방식의 구현은 추후 두 개 이상의 마이크로서비스가 통합되거나 하나의 마이크로서비스가 두 개 이상으로 분리되는 경우 여기에 맞추어 클라이언트 코드를 수정하는 것이 매우 어렵습니다. 앞에서도 이야기 했지만, 클라이언트는 마이크로서비스에 요청을 보내기 위해서 모든 마이크로서비스의 호스트명은 물론 end_point를 알고 있어야 하며, 호스트정보가 변경되면 클라이언트가 가지고 있는 정보 역시 함께 수정해 주어야하기 때문입니다.

API GATEWAY를 이용한 해결책

API Gateway는 그 이름에서도 유추할 수 있듯이 서비스로 전달되는 모든 API 요청의 관문(Gateway) 역할을 하는 서버입니다. API Gateway는 시스템의 아키텍처를 내부로 숨기고 외부의 요청에 대한 응답만을 적합한 형태로 응답합니다. 즉, 클라이언트는 시스템 내부의 아키텍처가 마이크로서비스 형태로 되어있는지 모놀리틱 아키텍처로 구현되어 있는지를 알 필요가 없으며 서로 약속한 형태의 API 요청만을 서버로 보내면 알맞는 형태의 결과를 받을 수 있습니다.

모든 사용자의 API 요청은 그림 6과 같이 제일 먼저 API Gateway에 도착하게 됩니다. API Gateway는 받은 요청을 기반으로 필요한 마이크로서비스에 개별적인 요청을 다시 보내게 됩니다. 이렇게 각각의 마이크로서비스로부터 받은 응답들을 API Gateway는 다시 취합하여 클라이언트에게 전달하는 역할을 수행합니다. 이 과정에서 API Gateway는 사용자의 HTTP 요청을 마이크로서비스가 받을 수 있는 다른 형태의 프로토콜로 전환하는 역할을 수행하기도 합니다. 앞에서 살펴본 검색 포털의 메인화면을 예로 들어 설명하자면, 사용자의 클라이언트는 메인 페이지에 대한 요청을 API Gateway에 보내고, 이 요청을 받은 API Gateway는 해당 요청의 응답에 필요한 정보들을 사용자서비스, 뉴스 서비스, 실시간 급상승 검색어 서비스, 광고 서비스, 날씨 서비스등에 해당 결과들을 하나의 응답으로 취합한 뒤 클라이언트에게 다시 전달하게 됩니다.

 

그림 6. API Gateway를 활용한 마이크로서비스 아키텍처에서의 요청 처리

API Gateway는 이처럼 클라이언트의 요청을 일괄적으로 처리하는 역할 뿐만 아니라, 전체 시스템의 부하를 분산시키는 로드 밸런서의 역할, 동일한 요청에 대한 불필요한 반복작업을 줄일 수 있는 캐싱, 시스템상을 오고가는 요청과 응답에 대한 모니터링 역할도 수행할 수 있습니다.

이렇게 API Gateway를 이용하여 서비스 요청에 대한 처리를 하게되면 특정 서비스의 변경사항이 생기거나 서비스가 통합/분리 되더라도 클라이언트는 그 사실을 인지할 필요가 없으며 API Gateway 내부의 변경사항만으로 처리가 가능하게 됩니다. 이렇게 API Gateway를 이용하여 서비스 요청에 대한 처리를 하게되면 특정 서비스의 변경사항이 생기거나 서비스가 통합/분리 되더라도 클라이언트는 그 사실을 인지할 필요가 없으며 API Gateway 내부의 변경사항만으로 처리가 가능하게 됩니다. 이렇게 시스템 내부의 아키텍처를 숨길 수 있는(encapsulate) 특성이 API Gateway가 갖는 가장 큰 장점이라고 할 수 있습니다.

API Gateway는 다른 모든 것들과 마찬가지로 장점만을 갖는 것은 아닙니다. API Gateway가 갖는 대표적인 단점으로는 구현하고 관리해야하는 요소가 하나 더 증가한다는 점입니다. 예를 들어, 특정 마이크로서비스에 기능이 추가되는 경우, API Gateway가 포함된 해당 기능을 사용자에게 전달하기 위한 내용을 API Gateway에도 반영해 주어야 합니다. 이러한 이유 때문에 API Gateway를 관리하는 절차가 최소화 되어야만 합니다.

API Gateway가 갖는 또 하나의 단점은 성능상의 병목(bottleneck)지점이 될 수 있다는 점입니다. 앞에서 설명한 바와 같이 API Gateway는 모든 요청을 수용해야 하는 창구 역할을 하기 때문에 이 부분에 병목현상이 발생하면 서비스 전체의 품질에 지대한 영향을 미치게 됩니다. 이러한 이유 때문에 API Gateway를 설계하고 구현할 때에는 항상 성능과 확장성을 고려해야만 합니다. 특히, API Gateway를 비동기(asynchronous)적이고 non-blocking I/O 처리가 가능하도록 구현하는 것이 적은 비용으로 최대의 성능을 발휘할 수 있는 관건이 됩니다.

API GATEWAY 구현시 고려해야할 점들

앞서 살펴본 검색 포털의 메인화면을 잘 살펴보면 각각의 서비스들이 서로 독립적으로 동작가능하다는 점을 알 수 있습니다. 이러한 경우 API Gateway는 동시에 여러개의 요청을 각각의 마이크로서비스에 전달하는 것이 가능합니다. 하지만, 모든 요청이 이와 같이 병렬로 처리될 수 있는 것은 아닙니다. 예를 들어, 검색 포털에서 카페의 메인화면을 표시하기 위해서 API Gateway는 먼저 ‘사용자 서비스’에 요청을 보내어 사용자 정보를 가져와야 하며, 이 정보를 기반으로 해당 사용자가 어떤 까페에 가입화여 활동중인지를 ‘카페 서비스’에 요청해야 합니다. 즉, 개별 마이크로서비스로 보내는 요청의 선후관계가 존재하게 되는 것입니다. 이러한 경우 일반적인 비동기처리를 위해서 콜백(callback) 함수를 이용하게됩니다. 즉, 사용자 서비스로 부터 정보를 받아온 뒤 이 요청의 콜백함수에서 카페 서비스로 다시 요청을 보내게 되는 것입니다. 이러한 방식의 애플리케이션 코드는 흔히 말하는 콜백 지옥(callback hell)을 경험하게 할 수 있으며 유지보수와 소스코드의 가독성을 현저하게 악화시키는 원인이 되곤 합니다.

이러한 문제점 때문에 많은 언어와 프레임워크들은 동일한 처리를 수행하는 코드를 좀 더 직관적이고 서술적으로 표현할 수 있는 Reactive 프로그래밍을 고안하게 되었습니다. 대표적인 reactive 프로그래밍 방식으로는 자바스크립트의 Promise, 스칼라의 Future등이 있습니다. 이러한 reactive 접근방식을 잘 활용하여 API Gateway를 구현하게 되면 비동기 처리의 성능적인 이점은 유지하면서 좀 더 직관적이고 관리하기 쉬운 코드를 작성할 수 있습니다.

API Gateway를 구현할 때 고려해야하는 또 한 가지 중요한 점은 예외에 대한 처리입니다. 마이크로서비스 아키텍처에서는 모놀리틱 아키텍처에 비하여 분산된 서버의 상태와 여러가지 변수들로 인해서 일부 서비스에 장애가 발생하거나 응답속도가 지연될 가능성이 높아지게 됩니다. 이렇게 특정서비스에 문제가 발생했을 때 API Gateway는 서비스 장애의 종류에 따라 적절한 처리가 가능하도록 설계/구현되어야 합니다. 예를 들어, 카페 메인화면에 대한 요청을 처리하는 과정에서 특정 서비스에 장애가 발생하는 상황을 예로 들어보도록 하겠습니다. 카페 메인화면에는 사용자 정보와 사용자가 가입한 카페 목록, 그리고 사용자가 관심을 가질만한 추천 카페 목록을 표시한다고 가정해 보겠습니다. 이 과정에서 ‘추천 카페 목록’을 제공하는 ‘추천 서비스’에서 장애가 발생하는 경우 일반적으로 카페 메인화면은 정상적으로 표시가 되고 추천 목록에는 임시적인 페이지로 대체되는 것이 적당할 것 입니다. 반면, 카페 목록을 제공해야 하는 ‘카페 서비스’에 장애가 발생하는 경우에는 가장 중요한 정보를 전달할 수 없기 때문에 오류 처리를 하는 것이 적합할 것 입니다. 이처럼 API Gateway는 특정 요청에 대하여 개별 서비스의 특수한 장애 상황에 대해서 어떻게 대처할지에 대한 고려가 구현시 포함되어야 합니다.

마지막으로 API Gateway는 모든 요청이 몰리는 지점이기 때문에 동일한 요청에 대하여 중복적으로 마이크로서비스에 요청을 보내는 것 보다 기존의 결과를 캐싱하여 재활용할 수 있도록 설계하는 것이 중요합니다. 예를 들어, 검색 포털의 메인화면에서 ‘날씨 서비스’와 같은 경우, 모든 요청에 대하여 반복적으로 현재 날씨를 가져오는 것 보다 주기적인 시간 단위로 갱신을 하며 한번 가져온 결과를 해당 주기 내에서 재활용하는 것이 불필요한 자원사용을 막는 현명한 방법이라고 할 수 있겠습니다.


이벤트-주도(Event-Driven) 데이터 관리

마이크로서비스 아키텍처에서 고려해야 하는 여러 가지 요소 중에서 많은 사람들이 가장 어려워하는 부분은 아마도 분산된 데이터베이스의 트랜잭션을 어떻게 관리할지에 관한 내용일 것입니다. 이번 절에서는 마이크로서비스 아키텍처에서 분산된 데이터를 어떻게 관리하는지 살펴보겠습니다.

분산된 데이터베이스에서 트랜잭션 관리 이슈

분산된 데이터베이스에서 트랜잭션 관리 이슈를 이해하려면 먼저, 모놀리틱 아키텍처의 단일 데이터베이스에서 트랜잭션의 관리가 어떻게 이루어지는지를 이해할 필요가 있습니다.일반적으로 단일 데이터베이스는 트랜잭션의 안정성을 네 가지 성격으로 분류하여 보장하며 이를 ACID 속성이라고 말합니다.

ACID(Atomicity, Consistency, Isloation, Durability)

모놀리틱 아키텍처에서 단일 데이터베이스를 사용함으로써 얻을 수 있는 가장 큰 장점은 바로 ACID 트랜잭션이 가능하다는 점입니다. ACID의 각 속성들이 무엇을 의미하는지 알아보도록 하겠습니다.

  • Atomicity(원자성) : 하나의 트랜잭션에 포함되는 여러 가지 작업들은 반드시 함께 성공하거나, 함께 실패해야 함을 의미합니다. 예를 들어, 계좌이체 트랜잭션의 경우 출금 작업과 입금 작업 중 일부만 성공해서는 안 되고 반드시 함께 성공하거나, 함께 실패해야 합니다.
  • Consistency(일관성) : 일관성은 트랜잭션을 통한 결과가 언제나 데이터베이스의 유효한 상태를 보장함을 의미합니다. 여기에서 말하는 유효함이란, 각종 제약조건(constaints), 캐스캐이딩(cascading)과 같은 규칙을 모두 만족하는 상태를 말하며, 이러한 조건에 어긋나는 트랜잭션은 중단됩니다.
  • Isolation(고립성) : 고립성은 하나의 트랜잭션이 다른 트랜잭션의 영향을 받지 않고 독립적으로 고립된 상태에서 수행됨을 보장합니다. 즉, 동시에 수행되는 여러 개의 트랜잭션의 결과는 각 트랜잭션을 순차적으로 실행했을 때의 결과와 같음을 의미합니다.
  • Durability(지속성) : 지속성은 데이터베이스에 한 번 커밋된 트랜잭션이 어떠한 장애상황에서도 유실되지 않고 유지됨을 보장합니다. 이를 위해서 트랜잭션의 결과는 비휘발성(non-volatile) 저장소에 기록되야 합니다.

단일 데이터베이스를 통해서 애플리케이션의 데이터를 관리하게 되면, 이러한 속성들을 보장하는 ACID 트랜잭션이 가능하므로 개발자는 데이터의 유실이나 정합성을 크게 고려하지 않고 개발을 수행할 수 있습니다.

 

다양한 종류의 데이터 저장소 (polyglot persistence)

아쉽게도 마이크로서비스 아키텍처에서는 단일 데이터베이스를 이용하는 것보다, 개별 마이크로서비스가 관리하는 데이터가 독립적인 데이터베이스에서 따로 관리되며, 이 데이터에 접근하려면 반드시 해당 마이크로서비스의 API를 통하는 것이 좋습니다. 이러한 구조를 유지해야만 각각의 마이크로서비스가 서로 느슨하게 연결(loosely coupled)되며, 독립적으로 개발 및 운영되는 것이 가능하기 때문입니다. 그뿐만 아니라, 개별 서비스마다 독립된 데이터베이스를 가져야만 해당 마이크로서비스 데이터의 특성에 가장 효율적인 저장소를 선택할 수 있습니다. 예를 들어, 검색 서비스를 제공하는 마이크로서비스의 데이터는 관계형 데이터베이스(RDBMS)보다 엘라스틱서치(Elasticsearch)와 같은 분산 검색엔진에 저장되어 관리되는 것이 바람직할 수 있고, 소셜 미디어의 친구관계에 대한 서비스를 제공하는 마이크로서비스의 경우 데이터를 Neo4j와 같은 그래프 데이터에 특화된 저장소에 넣어 관리하는 것이 바람직할 수 있습니다.

결론적으로, 마이크로서비스 아키텍처의 장점을 제대로 활용하려면 다양한 종류의 데이터 저장소(polyglot persistence)에서 독립적으로 데이터를 관리해야하며, 이것은 다시말해, 앞서 살펴본 ACID 트랜잭션의 장점을 이용할 수 없다는 것을 의미합니다. 특히, 분산된 데이터 관리에서 주로 발생하는 문제는 데이터의 정합성 이슈입니다. 이해를 돕기 위해서 그림 7과 같은 상황을 가정해 보도록 하겠습니다.

 

그림 7. 분산된 데이터베이스에서의 트랜잭션 관리 이슈

A라는 사용자는 온라인 쇼핑몰에서 물품 구매를 위해 주문을 요청합니다. 이 요청은 애플리케이션의 주문 서비스에 전달되며, 주문 서비스는 레코드를 생성하기에 앞서 결제 서비스 API를 호출하여 사용자 A의 계좌로부터 물품 대금을 출금합니다. 이 과정에서 잔액 부족과 같은 이유로 결제가 실패하면 주문 레코드는 생성되지 않고 사용자에게 오류 메시지를 전달하게 됩니다. 여기에서 문제는 결제가 정상적으로 이루어지고 계좌에서 금액이 출금된 이후에 주문 레코드의 생성과정에서 오류가 발생했을 때입니다. 모놀리틱 아키텍처에서는 이러한 과정이 하나의 트랜잭션 내에서 원자성이 보장되지만 분산된 데이터베이스에서는 결제만 되고 주문 레코드가 생성되지 않는 상황이 발생할 수 있습니다.

2단계 커밋(Two-Phase Commit : 2PC)

분산된 데이터베이스에서 발생할 수 있는 트랜잭션 문제의 전통적인 해결책으로는 2단계 커밋이 있습니다. 2단계 커밋이란, 분산된 트랜잭션의 원자성을 보장하기 위하여 고안된 프로토콜의 구현체이며, 코디네이터 서버는 트랜잭션의 시작과 함께 다른 서버에 트랜잭션 요청을 보내고, 각각의 서버는 트랜잭션이 완료됨에 따라 커밋할 준비가 완료되었다는 사실을 코디네이터 서버에 보냅니다(준비 단계: Prepare phase). 모든 서버의 커밋 준비가 완료되면 코디네이터 서버는 전체 서버에게 커밋을 수행하라고 요청하며, 만약 하나의 서버에서라도 문제가 발생하면 모든 데이터베이스는 전체 트랜잭션의 시작 전 상태로 롤백(rollback)됩니다(커밋 단계: Commit phase).

2PC의 가장 큰 문제점은 NoSQL을 포함한 대부분의 최근 기술들이 2PC를 지원하지 않는다는 점입니다. 즉, 다양한 종류의 데이터 저장소로 구성된 마이크로서비스 아키텍처에서는 2PC를 활용하여 분산 트랜잭션을 관리하는 것이 사실상 불가능하다는 점을 의미합니다. 이러한 이유 때문에 2PC에 도움 없이 분산 트랜잭션을 관리하기 위한 다양한 기법과 기술들이 계속 발전하고 있습니다.

이벤트 주도 아키텍처(EVENT-DRIVEN ARCHITECTURE)

이벤트 주도 아키텍처는 2PC 없이도 분산 데이터베이스의 트랜잭션 문제를 해결하기 위해서 고안된 대표적인 아키텍처입니다. 아래에서 자세하게 설명하겠지만, 이벤트 주도 아키텍처를 한마디로 설명하자면 ‘분산된 데이터베이스, 또는 시스템들이 이벤트의 발행(publish) 및 구독(subscribe)을 통해 트랜잭션이나 연산 처리를 할 수 있도록 만들어진 아키텍처’를 말합니다. 즉, 전체 서비스를 통제하는 작업이 이벤트 주도하에 이루어지기 때문에 이러한 이름을 갖고 있습니다. 좀 더 쉽고 정확하게 이벤트 주도 아키텍처를 이해하기 위해서 앞서 살펴본 온라인 쇼핑몰의 주문 트랜잭션 예시를 다시 한번 살펴보도록 하겠습니다.

 

그림 8. 이벤트 주도(Event-Driven) 아키텍처에서의 분산 트랜잭션 관리

그림 8과 같이 사용자 A가 물품 i에 대하여 주문을 요청하게 되면 주문 서비스는 우선 신규 주문에 대한 준비 레코드를 기록하고 이에 해당하는 이벤트를 발행합니다. 이 이벤트는 메시지 브로커(Message Broker)에 전달되며 해당 이벤트를 구독하고 있던 결제 서비스는 발행된 이벤트를 수신하여 결제와 출금 트랜잭션을 수행하게 됩니다. 이 작업이 성공하게 되면 결제 서비스는 다시 메시지 브로커에 결제 성공에 대한 이벤트를 발행하게 되고, 이를 구독하고 있던 주문 서비스가 다시 이벤트를 수신하여 주문 생성을 완료하게 됩니다.

이벤트 주도 아키텍처는 메시지 브로커에 전달된 이벤트가 최소 한 번 이상 전송된다는 가정하에 마이크로서비스 간의 직접적인 통신 없이도 분산된 데이터베이스의 트랜잭션 관리가 가능합니다. 그러나 그림 8의 처리 단계에서 볼 수 있듯이 프로그래밍이 복잡해질 뿐만 아니라, 애플리케이션의 실패에 대비하여 트랜잭션을 복원할 수 있는 기능을 구현해 두어야 한다는 단점이 있습니다. 또한, 이벤트를 구독한 서비스에서도 중복된 이벤트를 감지하여 이를 무시할 수 있는 기능이 구현되어야 합니다.

이벤트 주도 아키텍처에서 가장 중요하게 다뤄야 하는 부분은 바로 트랜잭션의 원자성(atomicity)을 보장하는 것입니다. 앞에서도 설명했지만, 트랜잭션의 원자성이란, 하나의 트랜잭션에 포함된 여러 개의 작업들이 부분적으로 성공하거나, 또는 부분적으로 실패하지 않도록 보장하는 것을 의미합니다. 예를 들어, 계좌 이체를 처리하는 트랜잭션에서는 보내는 사람의 계좌에서 금액을 빼는 작업과 받는 사람의 계좌에서 금액을 더하는 작업이 반드시 함께 성공하거나 함께 실패해야 합니다. 이벤트 주도 아키텍처에서 트랜잭션을 보장하기 위한 방법으로는 ‘로컬 트랜잭션을 이용하는 방법’, ‘트랜잭션 로그를 이용하는 방법’, ‘이벤트 소싱’ 등이 있습니다. 각각의 특징을 살펴보고 어떠한 장단점이 있는지 알아보도록 하겠습니다.

 

로컬 트랜잭션(local transaction)을 이용한 방법

이벤트 주도 아키텍처에서 트랜잭션의 원자성을 보장하기 위한 방법 중 하나로 로컬 트랜잭션을 이용한 이벤트의 발행이 있습니다. 이것은 로컬 트랜잭션에서 ACID 보장이 가능하다는 점을 이용해서 특정 테이블의 레코드가 갱신될 때 이벤트 테이블에 해당 이벤트 정보를 기록하는 방식입니다. 기록된 이벤트 정보는 이벤트 퍼블리셔(Event Publisher)에 의해서 메시지 브로커에 발행되며 발행된 이벤트는 다시 이벤트 테이블에 상태가 갱신됩니다. 이 방식은 로컬 트랜잭션의 ACID 특성으로 인해서, 레코드가 갱신되는 경우 반드시 이벤트가 발행되는 것을 보장할 수 있습니다. 반면, 레코드 갱신 시 이벤트 테이블에 관련 정보를 추가해야 한다는 점을 개발자가 기억하고 있어야 하기 때문에 실수할 가능성이 매우 높습니다.

 

그림 9. 로컬 트랜잭션(local transaction)을 이용한 이벤트 관리

트랜잭션 로그(transaction log)를 이용하는 방법

트랜잭션의 원자성을 보장하기 위한 또 한 가지 방법으로 데이터베이스의 트랜잭션 로그를 이용하는 방법이 있습니다. 대부분의 ACID 트랜잭션을 보장하는 데이터베이스는 갱신된 내용을 트랜잭션 로그에 기록하여 지속성(Durability)을 보장합니다. 앞서 살펴본 로컬 트랜잭션을 이용하는 방식에서 이벤트 퍼블리셔가 이벤트의 발행을 담당하였다면 이번 방식에서는 트랜잭션 로그 마이너(Transaction log miner)가 그 역할을 담당합니다. 이벤트 발행을 보장하는 원리는 앞에서 살펴본 방식과 크게 다르지 않습니다. 다만, 트랜잭션 로그 마이너는 이벤트 테이블이 아닌 트랜잭션 로그의 변경을 감지하여 이벤트를 발행합니다. 실제로 많은 서비스에서 이러한 방식으로 이벤트 주도 아키텍처를 구성하여 분산된 데이터베이스의 트랜잭션을 관리합니다.

트랜잭션 로그를 이용한 이벤트 발행은 로컬 트랜잭션을 이용한 방식과 마찬가지로 2PC의 도움 없이 이벤트의 발행을 보장할 수 있다는 장점이 있습니다. 그뿐만 아니라, 비즈니스 로직 내부에 이벤트 발행을 위한 별도의 처리가 필요하지 않기 때문에 애플리케이션 구조를 단순하게 유지할 수 있다는 장점이 있습니다. 그러나 이 방식은 데이터베이스의 특성에 따라 트랜잭션 로그의 형식이 각기 다를 수 있다는 점과 해당 로그의 내용만으로 비즈니스 이벤트를 추출해야 하는 어려움을 가지고 있습니다.

 

이벤트 소싱(Event Sourcing)

마지막으로 알아볼 방식은 이벤트 소싱입니다. 이벤트 소싱은 앞에서 살펴본 두 가지 방식과 달리 순수하게 이벤트에만 의존하여 원자성을 보장하는 방식입니다. 이벤트 소싱은 비즈니스 객체를 저장하기 위해서 이벤트 중심의 접근 방식을 사용합니다. 애플리케이션은 객체의 현재 상태를 저장하는 것이 아니라 상태 변화에 대한 일련의 이벤트를 저장합니다.

얼핏 보면 비즈니스 객체의 현재 상태를 조회하기 위해 저장된 레코드를 이용하는 방법과 해당 객체의 상태 변화 중 가장 최신 상태를 조회하는 방식이 어떤 차이를 가졌는지 쉽게 이해되지 않을 수도 있습니다. 물론, 애플리케이션에서 해당 객체의 가장 최신 값만 필요하다면 큰 차이가 없을 수 있습니다. 그러나 이벤트 주도 아키텍처에서 원자성을 보장하고 누락된 이벤트 처리를 방지하기 위해서는 특정 객체의 최신 상태뿐만 아니라, 그 상태에 이르기까지 어떤 변화를 어떠한 순서로 거쳤는지를 알아야만 합니다. 즉, 애플리케이션은 객체의 상태 변화와 관련된 일련의 이벤트를 재실행하면서 객체의 현재 상태를 재구성하는 것이 가능합니다.

비즈니스 객체 상태가 변경되면 언제나 새로운 이벤트가 이벤트 목록에 추가됩니다. 이벤트를 저장하는 것은 단일 연산이기 때문에 이것은 본질적으로 원자성을 갖습니다. 이벤트 소싱이 어떠한 방식으로 동작하는지 이해하기 위해서 앞서 살펴본 주문 예시를 다시 살펴보도록하겠습니다. 전통적인 접근방식에서 각각의 주문정보는 데이터베이스 내 주문 테이블에 하나의 열(row)과 매핑됩니다. 그러나 이벤트 소싱 방식에서는 데이터베이스 테이블에 주문 정보의 상태 변화 이벤트(생성, 결제 완료, 배송, 취소 등)가 저장됩니다.

그림 10에서도 볼 수 있지만, 이벤트 소싱 방식에서 특이한 점은 별도의 메시지 브로커가 존재하지 않는다는 점입니다. 대신, 이벤트 소싱은 이벤트 스토어(Event Store)라는 저장소를 통해 객체의 상태변화에 대한 이벤트를 저장하거나 조회할 수 있는 API를 제공합니다. 이를 통해, 이벤트 스토어는 메시지 브로커의 역할을 대체하게 됩니다. 이처럼 이벤트 스토어는 상태변화 이벤트를 저장함은 물론, 해당 이벤트를 구독하고 있는 서비스들에 새로운 이벤트를 전달하는 것과 같은 이벤트 주도 아키텍처에서 가장 근간이 되는 역할을 수행합니다.

 

그림 10. 상태 변화 이벤트 저장을 이용한 이벤트 소싱

이벤트 소싱 방식에서 발생할 수 있는 문제는 상태 변화 이벤트의 수가 많아질수록 성능이 매우 느려질 수 있다는 점입니다. 이를 해결하려면 특정 시점에 이전 이벤트의 누적정보를 스냅샷 형태로 저장할 수 있습니다. 또한, 이벤트를 저장하는 명령(Command)은 이벤트 스토어에 기록하고, 해당 이벤트의 결과인 객체의 현재 상태에 대한 질의(Query)는 별도의 데이터 저장소를 통해서 수행하는 ‘명령과 질의의 책임 분리(Command Query Responsibility Segregation : CQRS )’ 방법을 이용할 수 있습니다.

 

그림 11. 명령과 질의의 책임 분리(Command Query Responsibility Segregation : CQRS)

 

이벤트 소싱 방식이 갖는 대표적인 단점은 기존의 데이터 위주 프로그래밍 방식에 익숙한 개발자들에게는 다소 생소한 방식이기 때문에 학습 기간(learning curve)가 필요하다는 점입니다. 또한, 매우 단순한 모델을 구현하기 위해서 복잡한 아키텍처를 설계해야 하므로 배보다 배꼽이 더 크게 느껴질 수 있습니다. 그러나 이벤트 소싱의 장점은 도메인 모델이 복잡해질수록 오히려 변경에 대한 유연한 대처가 가능해집니다.

만약, 이벤트 소싱에 대한 좀 더 자세한 내용과 실제로 이것을 어떻게 구현하는지 궁금하시다면 마틴 파울러(Martin Fowler)가 정리한 이벤트 소싱 관련 글을 참고하실 수 있습니다.


Summary

이 글에서는 서비스 규모가 커지고 애플리케이션 구조가 복잡해짐에 따라 발생할 수 있는 비효율성을 해결하기 위한 하나의 방법으로 마이크로서비스 아키텍처(MSA)에 대해서 설명하였습니다. 마이크로서비스 아키텍처는 하나의 애플리케이션을 독립으로 동작할 수 있는 여러 개의 작은 서비스들의 집합으로 개발하는 방식과 각각의 서비스는 HTTP API와 같은 방식으로 서로 통신합니다.

마이크로서비스 아키텍처에서 빠질 수 없는 핵심 요소 중 하나는 바로 API Gateway입니다. API Gateway는 클라이언트의 요청을 일괄적으로 처리하는 창구역할을 하며, 전체 시스템의 부하를 분산시키는 로드밸런서의 역할도 하게 됩니다. API Gateway를 이용하면 실제 애플리케이션의 내부 아키텍처를 숨길 수 있기(encapsulate) 때문에 특정 서비스에 변경사항이 발생하더라도 관련된 전체적으로 애플리케이션에 미치는 영향을 최소화할 수 있습니다.

마이크로서비스 아키텍처의 대표적인 장점으로는 애플리케이션의 변경과 배포가 쉽고 효율적으로 특정 서비스를 스케일 아웃할 수 있어서 불필요한 자원의 낭비를 줄일 수 있다는 점이 있습니다. 또한, 애플리케이션의 아키텍처 변경이 쉽도록 설계되어있기 때문에 궁극적으로 개발 조직, 나아가서 회사의 조직 문화가 애자일한 방식으로 동작 가능하도록 영향을 미칠 수 있게 됩니다.

마이크로서비스 아키텍처의 장점을 제대로 활용하려면 각각의 서비스별로 독립된 데이터 저장소를 가지고 있어야 합니다. 이렇게 데이터가 분산되어 저장되면 전통적인 방식의 ACID 트랜잭션이 어렵기 때문에 별도의 데이터 관리 방법이 필요하게 됩니다. 이렇게 분산된 데이터의 트랜잭션을 관리할 수 있는 대표적인 방법으로는 이벤트 주도 아키텍처(Event-Driven Architecture)가 있습니다.

이벤트 주도 아키텍처란, 분산된 데이터베이스, 또는 시스템들이 이벤트의 발행(publish) 및 구독(subscribe)을 통해 트랜잭션이나 연산 처리를 할 수 있도록 만들어진 아키텍처를 말합니다. 이벤트 주도 아키텍처는 특정 비즈니스 객체의 변경이 발생할 때 반드시 이벤트가 발행한다는 가정하에 데이터의 정합성이 보장되기 때문에 트랜잭션의 원자성을 보장하기 위해서는 이벤트의 발행을 보장할 수 있는 몇 가지 기법이 필요합니다.

이벤트의 발행을 보장할 수 있는 대표적인 방법으로는 ‘로컬 트랜잭션(Local Transaction)’을 이용하는 방법과 데이터베이스의 ‘트랜잭션 로그(Transaction Log)’를 이용하는 방법, 그리고 ‘이벤트 소싱(Event Sourcing)’과 같은 방법들이 있습니다.

로컬 트랜잭션을 이용하는 방법과 트랜잭션 로그를 이용하는 방법은 전통적인 2단계 커밋(Two-Phase Commit : 2PC)의 도움 없이도 이벤트의 발행을 보장할 수 있다는 장점이 있지만, 각각 개발자의 기억에 의존해야 하기 때문에 오류가 발생할 확률이 높고, 데이터베이스에 따라 트랜잭션 로그의 형식이 각각 다를 수 있다는 단점이 있습니다.

이벤트 소싱은 이 나머지 두 가지 방식의 문제점을 해결하면서 이벤트의 발행을 보장할 수 있는 또 한 가지 방법입니다. 이벤트 소싱은 객체의 상태 변화에 대한 일련의 이벤트를 저장함으로써 이벤트의 발행을 보장하는 방식입니다. 이 때문에 기존에 데이터 중심의 개발방식에 익숙한 개발자들에게는 다소 학습시간이 필요한 방식입니다.

이 글에서 알아본 내용들은 서비스의 규모가 커짐에 따라 반드시 모두 적용해야 하는 필수적인 것들은 아닙니다. 그러나 대부분의 조직은 애플리케이션의 구조가 복잡해짐에 따라 개발 조직과 의사결정 프로세스 등이 함께 복잡해지기 때문에 초창기 스타트업에서 경험할 수 있었던 빠르고 동적인 개발이 어려워지게 됩니다. 많은 사람들은 이러한 문제를 해결하기 위해서 다양한 방법론을 시도하지만, 근본적으로 복잡한 애플리케이션 구조의 해결 없이는 조직의 문화를 가볍게 유지하는 것이 어렵습니다. 이러한 이유로 복잡한 구조를 가볍게 유지할 수 있는 다양한 아키텍처 패턴을 미리 알아보고 준비하는 것은 장기적으로 모든 기술 담당자들에게 큰 도움이 될 것입니다.

반응형