마이크로서비스를 위한 이벤트 기반(Event-Driven) 데이터 관리

마이크로서비스로 애플리케이션을 구축하는 방법에 대한 시리즈의 다섯 번째 포스트입니다. 첫 번째 포스트에서는 마이크로서비스 아키텍처 패턴을 소개하고 마이크로서비스 사용의 장점과 단점에 대해 설명했습니다. 시리즈의 두 번째세 번째 포스트에서는 마이크로서비스 아키텍처 내 통신의 다양한 측면을 설명합니다. 네 번째 포스터에서는 밀접하게 관련된 서비스 발견 문제를 탐구합니다. 이 포스트에서는 마이크로서비스 아키텍처에서 발생하는 분산 데이터 관리 문제를 살펴봅니다.

목차

1. 마이크로서비스와 분산 데이터 관리의 문제
2. 이벤트 기반 아키텍처 (Event‑Driven Architecture)
3. 원자성 달성 (Achieving Atomicity)
3-1. 로컬 트랜잭션(Local Transactions)을 사용하여 이벤트 게시
3-2. 데이터베이스 트랜잭션 로그 마이닝 (Mining a Database Transaction Log)
3-3. 이벤트 소싱 사용 (Using Event Sourcing)
4. 요약

1. 마이크로서비스와 분산 데이터 관리의 문제

모놀리식 애플리케이션에는 일반적으로 단일 관계형 데이터베이스가 있습니다. 관계형 데이터베이스를 사용하는 주요 이점은 애플리케이션이 ACID 트랜잭션을 사용할 수 있다는 점이며, 이는 다음과 같은 몇 가지 중요한 보장을 제공합니다.

  • Atomicity(원자성) – 원자적으로 변경됩니다.
  • Consistency(일관성) – 데이터베이스의 상태는 항상 일관성이 있습니다.
  • Isolation(격리) – 트랜잭션이 동시에 실행되더라도 순차적으로 실행되는 것처럼 보입니다.
  • Durability(내구성) – 트랜잭션이 커밋되면 실행 취소되지 않습니다.

결과적으로 애플리케이션은 단순히 트랜잭션을 시작하고 여러 행을 변경(insert, update, delete)하고 트랜잭션을 커밋(commit)할 수 있습니다.

관계형 데이터베이스 사용의 또 다른 큰 이점은 풍부하고 선언적이며 표준화된 query 언어인 SQL을 제공한다는 것입니다. 여러 테이블의 데이터를 결합하는 query를 쉽게 작성할 수 있습니다. 그런 다음 RDBMS query 플래너는 query를 실행하는 가장 최적의 방법을 결정합니다. 데이터베이스에 액세스하는 방법과 같은 낮은 수준의 세부 정보에 대해 걱정할 필요가 없습니다. 또한 애플리케이션의 모든 데이터가 하나의 데이터베이스에 있기 때문에 query하기 쉽습니다.

안타깝게도 마이크로서비스 아키텍처로 전환하면 데이터 액세스가 훨씬 더 복잡해집니다. 각 마이크로서비스가 소유한 데이터는 해당 마이크로서비스 전용이며 해당 API를 통해서만 액세스할 수 있기 때문입니다. 데이터를 캡슐화하면 마이크로서비스가 느슨하게 결합되고 서로 독립적으로 발전할 수 있습니다. 여러 서비스가 동일한 데이터에 액세스하는 경우 스키마 업데이트에는 모든 서비스에 대해 시간이 많이 걸리고 조정된 업데이트가 필요합니다.

설상가상으로 서로 다른 마이크로서비스는 종종 서로 다른 종류의 데이터베이스를 사용합니다. 최신 애플리케이션은 다양한 종류의 데이터를 저장하고 처리하며 관계형 데이터베이스가 항상 최선의 선택은 아닙니다. 일부 사용 사례의 경우 특정 NoSQL 데이터베이스가 더 편리한 데이터 모델을 갖고 훨씬 더 나은 성능과 확장성을 제공할 수 있습니다. 예를 들어 Elasticsearch와 같은 텍스트 검색 엔진을 사용하기 위해 텍스트를 저장하고 쿼리하는 서비스가 적합합니다. 마찬가지로 소셜 그래프 데이터를 저장하는 서비스는 Neo4j와 같은 그래프 데이터베이스를 사용해야 합니다. 결과적으로 마이크로서비스 기반 애플리케이션은 SQL과 NoSQL 데이터베이스를 혼합하여 사용하는 경우가 많으며, 이른바 다중 언어 지속성 접근 방식입니다.

데이터 저장을 위한 분할된 다중 언어 지속 아키텍처는 느슨하게 결합된 서비스, 더 나은 성능 및 확장성을 포함하여 많은 이점을 제공합니다. 그러나 일부 분산 데이터 관리 문제가 발생합니다.

첫 번째 과제는 여러 서비스에서 일관성을 유지하는 비즈니스 트랜잭션을 구현하는 방법입니다. 이것이 왜 문제인지 알아보기 위해 온라인 B2B 상점의 예를 살펴보겠습니다. 고객 서비스는 신용 한도를 포함하여 고객에 대한 정보를 유지 관리합니다. 주문 서비스는 주문을 관리하고 새 주문이 고객의 신용 한도를 초과하지 않는지 확인해야 합니다. 이 애플리케이션의 모놀리식 버전에서 주문 서비스는 ACID 트랜잭션을 사용하여 사용 가능한 신용을 확인하고 주문을 생성할 수 있습니다.

대조적으로, 마이크로서비스 아키텍처에서 ORDER 및 CUSTOMER 테이블은 다음 다이어그램과 같이 해당 서비스에 대해 비공개입니다.

microservices separate tables

주문 서비스는 CUSTOMER 테이블에 직접 액세스할 수 없습니다. 고객 서비스에서 제공하는 API만 사용할 수 있습니다. 주문 서비스는 2단계 커밋(2PC)이라고도 하는 분산 트랜잭션을 잠재적으로 사용할 수 있습니다. 그러나 2PC는 일반적으로 최신 응용 프로그램에서 실행 가능한 옵션이 아닙니다. CAP 정리에서는 가용성과 ACID 스타일 일관성 중에서 선택해야 하며 일반적으로 가용성이 더 나은 선택입니다. 또한 대부분의 NoSQL 데이터베이스와 같은 많은 최신 기술은 2PC를 지원하지 않습니다. 서비스와 데이터베이스 전반에 걸쳐 데이터 일관성을 유지하는 것이 필수적이므로 다른 솔루션이 필요합니다.

두 번째 과제는 여러 서비스에서 데이터를 검색하는 query를 구현하는 방법입니다. 예를 들어 애플리케이션이 고객과 최근 주문을 표시해야 한다고 가정해 보겠습니다. 주문 서비스가 고객의 주문을 검색하기 위한 API를 제공하는 경우 애플리케이션 측 JOIN을 사용하여 이 데이터를 검색할 수 있습니다. 애플리케이션은 고객 서비스에서 고객을 검색하고 주문 서비스에서 고객의 주문을 검색합니다. 그러나 주문 서비스가 기본 키에 의한 주문 조회만 지원한다고 가정합니다(아마도 기본 키 기반 검색만 지원하는 NoSQL 데이터베이스를 사용). 이 상황에서는 필요한 데이터를 검색할 명확한 방법이 없습니다.

2. 이벤트 기반 아키텍처 (Event‑Driven Architecture)

많은 애플리케이션에서 솔루션은 이벤트 기반 아키텍처를 사용하는 것입니다. 이 아키텍처에서 마이크로서비스는 비즈니스 항목을 업데이트하는 경우와 같이 주목할만한 일이 발생할 때 이벤트를 게시합니다. 다른 마이크로서비스는 해당 이벤트를 구독합니다. 마이크로서비스가 이벤트를 수신하면 자체 비즈니스 엔터티를 업데이트할 수 있으므로 더 많은 이벤트가 게시될 수 있습니다.

이벤트를 사용하여 여러 서비스에 걸쳐 있는 비즈니스 트랜잭션을 구현할 수 있습니다. 트랜잭션은 일련의 단계로 구성됩니다. 각 단계는 비즈니스 항목을 업데이트하고 다음 단계를 트리거하는 이벤트를 게시하는 마이크로서비스로 구성됩니다. 다음 다이어그램 시퀀스는 주문 생성 시 사용 가능한 크레딧을 확인하기 위해 이벤트 기반 접근 방식을 사용하는 방법을 보여줍니다. 마이크로 서비스는 메시지 브로커를 통해 이벤트를 교환합니다.

1. 주문 서비스는 상태가 NEW인 주문을 생성하고 주문 생성 이벤트를 게시합니다.

microservices credit check

2. 고객 서비스는 주문 생성 이벤트를 사용하고 주문에 대한 신용을 예약하고 신용 예약 이벤트를 게시합니다.

microservices credit check

3. Order Service는 Credit Reserved 이벤트를 사용하고 주문 상태를 OPEN으로 변경합니다.

microservices credit check

더 복잡한 시나리오에는 고객의 신용이 확인되는 동시에 재고를 예약하는 것과 같은 추가 단계가 포함될 수 있습니다.

(a) 각 서비스가 데이터베이스를 원자적으로 업데이트하고 이벤트를 게시하고(나중에 자세히 설명) (b) Message Broker가 이벤트가 한 번 이상 전달되도록 보장하면 여러 서비스에 걸쳐 있는 비즈니스 트랜잭션을 구현할 수 있습니다. 이는 ACID 트랜잭션이 아니라는 점에 유의하는 것이 중요합니다. 그들은 최종 일관성과 같은 훨씬 약한 보장을 제공합니다. 이 트랜잭션 모델을 BASE 모델이라고 합니다.

또한 이벤트를 사용하여 여러 마이크로서비스가 소유한 데이터를 사전 결합하는 구체화된 보기를 유지할 수 있습니다. view를 유지 관리하는 서비스는 관련 이벤트를 구독하고 view를 업데이트합니다. 예를 들어, 고객 주문 보기를 유지 관리하는 고객 주문 보기 업데이트 서비스는 고객 서비스 및 주문 서비스에서 게시한 이벤트를 구독(subscribe)합니다.

microservices subscribe

고객 주문 보기 업데이트 서비스는 고객 또는 주문 이벤트를 수신하면 고객 주문 보기 데이터 저장소를 업데이트합니다. MongoDB와 같은 문서 데이터베이스를 사용하여 고객 주문 보기를 구현하고 각 고객에 대해 하나의 문서를 저장할 수 있습니다. 고객 주문 보기 query 서비스는 고객 주문 보기 데이터 저장소를 query하여 고객 및 최근 주문에 대한 요청을 처리합니다.

이벤트 기반 아키텍처에는 몇 가지 장점과 단점이 있습니다. 이는 여러 서비스에 걸쳐 있고 최종 일관성을 제공하는 트랜잭션의 구현을 가능하게 합니다. 또 다른 이점은 응용 프로그램에서 구체화된 view를 유지 관리할 수 있다는 것입니다. 한 가지 단점은 프로그래밍 모델이 ACID 트랜잭션을 사용할 때보다 더 복잡하다는 것입니다. 종종 애플리케이션 수준의 오류를 복구하기 위해 보상 트랜잭션을 구현해야 합니다. 예를 들어 신용 확인에 실패하면 주문을 취소해야 합니다. 또한 애플리케이션은 일관성 없는 데이터를 처리해야 합니다. 진행 중인 트랜잭션의 변경 사항이 표시되기 때문입니다. 애플리케이션은 아직 업데이트되지 않은 구체화된 view에서 읽는 경우에도 불일치를 볼 수 있습니다. 또 다른 단점은 가입자가 중복 이벤트를 감지하고 무시해야 한다는 것입니다.

3. 원자성 달성 (Achieving Atomicity)

이벤트 기반 아키텍처에는 데이터베이스를 원자적으로 업데이트하고 이벤트를 게시하는 문제도 있습니다. 예를 들어 주문 서비스는 ORDER 테이블에 행을 삽입하고 주문 생성 이벤트를 게시해야 합니다. 이 두 작업은 원자적으로 수행되어야 합니다. 데이터베이스를 업데이트한 후 이벤트를 게시하기 전에 서비스가 충돌하면 시스템이 일관되지 않게 됩니다. 원자성을 보장하는 표준 방법은 데이터베이스 및 메시지 브로커를 포함하는 분산 트랜잭션을 사용하는 것입니다. 그러나 CAP 정리와 같은 위에서 설명한 이유 때문에 이것은 정확히 우리가 원하지 않는 것입니다.

3-1. 로컬 트랜잭션(Local Transactions)을 사용하여 이벤트 게시

원자성(achieve)을 달성하는 한 가지 방법은 애플리케이션이 로컬 트랜잭션(Local Transactions)만 포함하는 다단계(multi-step) 프로세스를 사용하여 이벤트를 게시하는 것입니다. 비결은 비즈니스 엔터티의 상태를 저장하는 데이터베이스에 메시지 대기열의 기능을 하는 EVENT 테이블을 두는 것입니다. 응용 프로그램은 (로컬) 데이터베이스 트랜잭션을 시작하고, 비즈니스 항목의 상태를 업데이트하고, 이벤트를 EVENT 테이블에 삽입하고, 트랜잭션을 커밋(commit)합니다. 별도의 애플리케이션 스레드 또는 프로세스는 EVENT 테이블을 query하고 이벤트를 메시지 브로커에 게시한 다음 로컬 트랜잭션(Local Transactions)을 사용하여 이벤트를 게시(published)된 것으로 표시합니다. 다음 다이어그램은 디자인을 보여줍니다.

microservices local transaction

주문 서비스는 순서 테이블에 행을 삽입하고 작성된 이벤트를 이벤트 테이블에 삽입합니다. 이벤트 게시자 스레드 또는 프로세스는 게시되지 않은 이벤트의 이벤트 테이블을 query하고 이벤트를 게시 한 다음 이벤트 테이블을 업데이트하여 이벤트를 게시(published) 한대로 표시합니다.

이 접근 방식에는 몇 가지 장점과 단점이 있습니다. 한 가지 이점은 2PC에 의존하지 않고 각 업데이트에 대해 이벤트가 게시되도록 보장한다는 것입니다. 또한 애플리케이션은 비즈니스 수준 이벤트를 게시하므로 이를 유추할 필요가 없습니다. 이 접근 방식의 한 가지 단점은 개발자가 이벤트 게시를 기억해야 하기 때문에 잠재적으로 오류가 발생하기 쉽다는 것입니다. 이 접근 방식의 한계는 제한된 트랜잭션 및 쿼리 기능으로 인해 일부 NoSQL 데이터베이스를 사용할 때 구현하기 어렵다는 것입니다.

이 접근 방식은 응용 프로그램이 상태를 업데이트하고 이벤트를 게시하기 위해 로컬 트랜잭션을 사용하도록 함으로써 2PC의 필요성을 제거합니다. 이제 애플리케이션이 단순히 상태를 업데이트하도록 하여 원자성을 달성하는 접근 방식을 살펴보겠습니다.

3-2. 데이터베이스 트랜잭션 로그 마이닝 (Mining a Database Transaction Log)

2PC 없이 원자성을 달성하는 또 다른 방법은 데이터베이스의 트랜잭션 또는 커밋 로그를 마이닝하는 스레드 또는 프로세스에서 이벤트를 게시하는 것입니다. 응용 프로그램은 데이터베이스를 업데이트하므로 변경 사항이 데이터베이스의 트랜잭션 로그에 기록됩니다. Transcation Log Miner thread 또는 프로세스는 트랜잭션 로그를 읽고 이벤트를 메시지 브로커에 게시합니다. 다음 다이어그램은 디자인을 보여줍니다.

microservices transaction log

이 접근 방식의 예는 오픈소스 LinkedIn Databus 프로젝트입니다. Databus는 Oracle 트랜잭션 로그를 마이닝하고 변경 사항에 해당하는 이벤트를 게시합니다. LinkedIn은 Databus를 사용하여 다양한 파생 데이터 저장소를 기록 시스템과 일관되게 유지합니다.

또 다른 예는 관리형 NoSQL 데이터베이스인 AWS DynamoDB의 스트림 메커니즘입니다. DynamoDB 스트림에는 지난 24시간 동안 DynamoDB 테이블의 항목에 적용된 변경 사항(생성, 업데이트 및 삭제 작업)의 시간 순서가 포함됩니다. 애플리케이션은 스트림에서 이러한 변경 사항을 읽고 예를 들어 이벤트로 게시할 수 있습니다.
트랜잭션 로그 마이닝에는 다양한 장점과 단점이 있습니다. 한 가지 이점은 2PC를 사용하지 않고 각 업데이트에 대해 이벤트가 게시되도록 보장한다는 것입니다. 트랜잭션 로그 마이닝은 이벤트 게시를 애플리케이션의 비즈니스 로직과 분리하여 애플리케이션을 단순화할 수도 있습니다. 주요 단점은 트랜잭션 로그 형식이 각 데이터베이스에 고유하며 데이터베이스 버전 간에도 변경될 수 있다는 것입니다. 또한 트랜잭션 로그에 기록된 하위 수준 업데이트에서 상위 수준 비즈니스 이벤트를 리버스 엔지니어링하기 어려울 수 있습니다.

트랜잭션 로그 마이닝은 응용 프로그램이 데이터베이스 업데이트라는 한 가지 작업을 수행하도록 하여 2PC의 필요성을 제거합니다. 이제 업데이트를 제거하고 이벤트에만 의존하는 다른 접근 방식을 살펴보겠습니다.

3-3. 이벤트 소싱 사용 (Using Event Sourcing)

이벤트 소싱은 비즈니스 엔터티를 유지하기 위해 근본적으로 다른 이벤트 중심 접근 방식을 사용하여 2PC 없이 원자성을 달성합니다. 응용 프로그램은 엔터티의 현재 상태를 저장하는 대신 상태 변경 이벤트의 시퀀스를 저장합니다. 애플리케이션은 이벤트를 재생하여 엔티티의 현재 상태를 재구성합니다. 비즈니스 항목의 상태가 변경될 때마다 새 이벤트가 이벤트 목록에 추가됩니다. 이벤트 저장은 단일 작업이므로 본질적으로 원자적입니다.

이벤트 소싱이 작동하는 방식을 보려면 주문 엔터티를 예로 들어 보겠습니다. 기존 접근 방식에서 각 주문은 ORDER 테이블의 행과 ORDER_LINE_ITEM 테이블과 같은 행에 매핑됩니다. 그러나 이벤트 소싱을 사용할 때 주문 서비스는 상태 변경 이벤트(Created, Approved, Shipped, Cancelled)의 형태로 주문을 저장합니다. 각 이벤트에는 주문 상태를 재구성하기에 충분한 데이터가 포함되어 있습니다.

microservices event sourcing

이벤트는 이벤트 데이터베이스인 이벤트 저장소에 유지됩니다. 상점에는 엔티티의 이벤트를 추가하고 검색하기 위한 API가 있습니다. 이벤트 저장소는 이전에 설명한 아키텍처의 메시지 브로커처럼 작동합니다. 서비스가 이벤트를 구독할 수 있도록 하는 API를 제공합니다. 이벤트 스토어는 관심 있는 모든 구독자에게 모든 이벤트를 전달합니다. 이벤트 저장소는 이벤트 기반 마이크로서비스 아키텍처의 백본(backbone)입니다.

이벤트 소싱에는 몇 가지 이점이 있습니다. 이벤트 기반 아키텍처 구현의 주요 문제 중 하나를 해결하고 상태가 변경될 때마다 이벤트를 안정적으로 게시할 수 있습니다. 결과적으로 마이크로서비스 아키텍처의 데이터 일관성 문제를 해결합니다. 또한 도메인 객체보다 이벤트를 지속하기 때문에 객체 관계형 임피던스 불일치 문제를 대부분 방지합니다. 이벤트 소싱은 또한 비즈니스 엔터티에 대한 변경 사항에 대한 100% 신뢰할 수 있는 감사 로그를 제공하고 특정 시점에서 엔터티의 상태를 결정하는 임시 쿼리를 구현하는 것을 가능하게 합니다. 이벤트 소싱의 또 다른 주요 이점은 비즈니스 논리가 이벤트를 교환하는 느슨하게 결합된 비즈니스 엔터티로 구성된다는 것입니다. 따라서 모놀리식 애플리케이션에서 마이크로서비스 아키텍처로 훨씬 쉽게 마이그레이션할 수 있습니다.

이벤트 소싱에도 몇 가지 단점이 있습니다. 그것은 다르고 익숙하지 않은 프로그래밍 스타일이므로 학습 곡선이 있습니다. 이벤트 저장소는 기본 키로 비즈니스 항목 조회만 직접 지원합니다. 쿼리를 구현하려면 명령 쿼리 책임 분리(CQRS)를 사용해야 합니다. 결과적으로 애플리케이션은 최종적으로 일관된 데이터를 처리해야 합니다.

4. 요약

마이크로서비스 아키텍처에서 각 마이크로 서비스에는 자체 개인 데이터 저장소가 있습니다. 다른 마이크로 서비스는 다른 SQL 및 NoSQL 데이터베이스를 사용할 수 있습니다. 이 데이터베이스 아키텍처에는 상당한 이점이 있지만 일부 분산 데이터 관리 문제가 발생합니다. 첫 번째 과제는 여러 서비스에서 일관성을 유지하는 비즈니스 트랜잭션을 구현하는 방법입니다. 두 번째 과제는 여러 서비스에서 데이터를 검색하는 쿼리를 구현하는 방법입니다.

많은 애플리케이션에서 솔루션은 이벤트 기반 아키텍처를 사용하는 것입니다. 이벤트 기반 아키텍처를 구현하는 데 있어 한 가지 문제는 상태를 원자적으로 업데이트하는 방법과 이벤트를 게시하는 방법입니다. 데이터베이스를 메시지 대기열로 사용, 트랜잭션 로그 마이닝 및 이벤트 소싱을 포함하여 이를 수행하는 몇 가지 방법이 있습니다.

향후 블로그 포스트에서 우리는 계속해서 마이크로서비스의 다른 측면에 대해 알아볼 것입니다.