클라우드 네이티브 데이터 패턴

이 콘텐츠는 Azure용 클라우드 네이티브 .NET 애플리케이션 설계 eBook 에서 발췌한 것으로, .NET 문서에서 제공되거나 오프라인 상태에서도 읽을 수 있는 PDF(무료 다운로드 가능)로 제공됩니다.

Cloud Native .NET apps for Azure eBook cover thumbnail.

이 책 전체에서 살펴보았듯이 클라우드 네이티브 방법은 애플리케이션을 디자인, 배포 및 관리하는 방식을 변경합니다. 또한 데이터를 관리하고 저장하는 방식도 변경됩니다.

그림 5-1은 차이점을 대조합니다.

Data storage in cloud-native applications

그림 5-1. 클라우드 네이티브 애플리케이션의 데이터 관리

숙련된 개발자는 그림 5-1의 왼쪽에 있는 아키텍처를 쉽게 인식할 수 있습니다. 이 모놀리식 애플리케이션에서 비즈니스 서비스 구성 요소는 공유 서비스 계층에 함께 배치되어 단일 관계형 데이터베이스의 데이터를 공유합니다.

여러 면에서 단일 데이터베이스는 데이터 관리를 단순하게 유지합니다. 여러 테이블에서 데이터를 쿼리하는 것은 간단합니다. 데이터 변경 내용이 함께 업데이트되거나 모두 롤백됩니다. ACID 트랜잭션은 강력하고 즉각적인 일관성을 보장합니다.

클라우드 네이티브를 위한 디자인은 다른 접근 방식을 취합니다. 그림 5-1의 오른쪽에서 비즈니스 기능이 어떻게 소규모의 독립적인 마이크로 서비스로 분리되는지 확인합니다. 각 마이크로 서비스는 특정 비즈니스 기능과 자체 데이터를 캡슐화합니다. 모놀리식 데이터베이스는 각각 마이크로 서비스에 맞춰 더 작은 데이터베이스가 많은 분산 데이터 모델로 분해됩니다. 연기가 사라지면 마이크로 서비스당 데이터베이스를 노출하는 디자인으로 등장합니다.

마이크로 서비스당 데이터베이스를 사용하는 이유는 무엇인가요?

이 마이크로 서비스당 데이터베이스는 특히 빠르게 발전하고 대규모 크기 조정을 지원해야 하는 시스템에 많은 이점을 제공합니다. 이 모델의 경우 다음과 같습니다.

  • 도메인 데이터는 서비스 내에서 캡슐화됩니다.
  • 데이터 스키마는 다른 서비스에 직접적인 영향을 미치지 않고 발전할 수 있습니다.
  • 각 데이터 저장소는 독립적으로 크기를 조정할 수 있습니다.
  • 한 서비스의 데이터 저장소 오류는 다른 서비스에 직접적인 영향을 미치지 않습니다.

또한 데이터를 분리하면 각 마이크로 서비스가 워크로드, 스토리지 요구 사항 및 읽기/쓰기 패턴에 가장 최적화된 데이터 저장소 형식을 구현할 수 있습니다. 선택 항목에는 관계형, 문서, 키-값 및 그래프 기반 데이터 저장소도 포함됩니다.

그림 5-2는 클라우드 네이티브 시스템에서 다중저장소 지속성의 원칙을 제시합니다.

Polyglot data persistence

그림 5-2. 다중저장소 데이터 지속성

이전 그림에서 각 마이크로 서비스가 서로 다른 형식의 데이터 저장소를 지원하는 방법에 유의합니다.

  • 제품 카탈로그 마이크로 서비스는 기본 데이터의 풍부한 관계형 구조를 수용하기 위해 관계형 데이터베이스를 사용합니다.
  • 쇼핑 카트 마이크로 서비스는 간단한 키-값 데이터 저장소를 지원하는 분산 캐시를 사용합니다.
  • 주문 마이크로 서비스는 쓰기 작업을 위한 NoSql 문서 데이터베이스와 대량의 읽기 작업을 수용하기 위해 고도로 비정규화된 키/값 저장소를 모두 사용합니다.

관계형 데이터베이스는 여전히 복잡한 데이터가 있는 마이크로 서비스와 관련이 있지만 NoSQL 데이터베이스는 상당한 인기를 얻고 있습니다. 대규모 및 고가용성을 제공합니다. 스키마가 없는 특성을 통해 개발자는 변경 비용과 시간이 많이 소요되는 형식화된 데이터 클래스 및 ORM 아키텍처에서 벗어날 수 있습니다. 이 장의 뒷부분에서 NoSQL 데이터베이스를 다룹니다.

데이터를 별도의 마이크로 서비스로 캡슐화하면 민첩성, 성능 및 확장성이 향상될 수 있지만 동시에 많은 문제가 발생합니다. 다음 섹션에서는 이러한 문제를 극복하는 데 도움이 되는 패턴 및 사례와 함께 설명합니다.

서비스 간 쿼리

마이크로 서비스는 독립적이며 재고, 배송 또는 주문과 같은 특정 기능에 중점을 두지만 다른 마이크로 서비스와의 통합이 필요한 경우가 많습니다. 종종 통합에는 하나의 마이크로 서비스가 다른 마이크로 서비스에서 데이터를 쿼리하는 작업이 포함됩니다. 그림 5-3은 시나리오를 보여 줍니다.

Querying across microservices

그림 5-3. 마이크로 서비스 간 쿼리

앞의 그림에서 사용자의 장바구니에 항목을 추가하는 장바구니 마이크로 서비스를 볼 수 있습니다. 이 마이크로 서비스의 데이터 저장소에는 장바구니 및 품목 데이터가 포함되지만 제품 또는 가격 책정 데이터는 유지 관리되지 않습니다. 대신 이러한 데이터 항목은 카탈로그 및 가격 책정 마이크로 서비스에서 소유합니다. 이 측면에서 문제가 있습니다. 장바구니 마이크로 서비스가 데이터베이스에 제품이나 가격 책정 데이터가 없을 때 어떻게 사용자의 장바구니에 제품을 추가할 수 있나요?

4장에서 설명하는 한 가지 옵션은 장바구니에서 카탈로그 및 가격 책정 마이크로 서비스로의 직접 HTTP 호출입니다. 그러나 4장에서는 동기 HTTP 호출이 마이크로 서비스를 함께 호출하여 자율성을 줄이고 아키텍처 이점을 감소시킨다고 설명했습니다.

각 서비스에 대해 별도의 인바운드 및 아웃바운드 큐를 사용하여 요청-회신 패턴을 구현할 수도 있습니다. 그러나 이 패턴은 복잡하고 요청 및 응답 메시지를 연관시키기 위해 연결이 필요합니다. 백 엔드 마이크로 서비스 호출을 분리하는 동안 호출 서비스는 호출이 완료될 때까지 동기적으로 기다려야 합니다. 네트워크 정체, 일시적인 오류 또는 과부하된 마이크로 서비스로 인해 장기간 실행되거나 작업이 실패할 수도 있습니다.

대신, 서비스 간 종속성을 제거하기 위해 널리 사용되는 패턴은 그림 5-4와 같이 구체화된 뷰 패턴입니다.

Materialized view pattern

그림 5-4. 구체화된 뷰 패턴

이 패턴을 사용하여 장바구니 서비스에 로컬 데이터 테이블(읽기 모델이라고 함)을 배치합니다. 이 테이블에는 제품 및 가격 책정 마이크로 서비스에 필요한 데이터의 비정규화된 복사본이 포함되어 있습니다. 데이터를 장바구니 마이크로 서비스에 직접 복사하면 비용이 많이 드는 서비스 간 호출이 필요하지 않습니다. 서비스에 대한 로컬 데이터를 사용하면 서비스의 응답 시간과 안정성이 개선됩니다. 또한 자체 데이터 복사본을 사용하면 장바구니 서비스의 복원력이 향상됩니다. 카탈로그 서비스를 사용할 수 없게 되는 경우 장바구니 서비스에 직접적인 영향을 미치지 않습니다. 장바구니는 자체 스토어의 데이터로 계속 작동할 수 있습니다.

이 방법을 통해 이제 시스템에 중복 데이터가 있다는 것을 알아냈습니다. 그러나 클라우드 네이티브 시스템에서 데이터를 전략적으로 복제하는 것은 정립된 사례이며 안티 패턴 또는 잘못된 사례로 간주되지 않습니다. 하나의 서비스만 데이터 집합을 소유할 수 있으며 데이터 집합에 대한 권한을 가질 수 있습니다. 레코드 시스템이 업데이트되면 읽기 모델을 동기화해야 합니다. 동기화는 일반적으로 그림 5.4와 같이 게시/구독 패턴이 있는 비동기 메시징을 통해 구현됩니다.

분산 트랜잭션

마이크로 서비스에서 데이터를 쿼리하는 것은 어렵지만 여러 마이크로 서비스에서 트랜잭션을 구현하는 것은 훨씬 더 복잡합니다. 서로 다른 마이크로 서비스의 독립적인 데이터 원본에서 데이터 일관성을 유지하는 고유한 과제는 과소 평가될 수 없습니다. 클라우드 네이티브 애플리케이션에서 분산 트랜잭션이 없으면 프로그래밍 방식으로 분산 트랜잭션을 관리해야 합니다. 즉각적 일관성의 세계에서 최종 일관성의 세계로 이동합니다.

그림 5-5는 문제를 보여 줍니다.

Transaction in saga pattern

그림 5-5. 마이크로 서비스 간 트랜잭션 구현

앞의 그림에서 5개의 독립적인 마이크로 서비스가 주문을 만드는 분산 트랜잭션에 참여합니다. 각 마이크로 서비스는 자체 데이터 저장소를 유지 관리하고 해당 저장소에 대한 로컬 트랜잭션을 구현합니다. 주문을 만들려면 개별 마이크로 서비스에 대한 로컬 트랜잭션이 성공해야 합니다. 그렇지 않으면 모두 작업을 중단하고 롤백해야 합니다. 각 마이크로 서비스 내에서 기본 제공 트랜잭션 지원을 사용할 수 있지만 데이터를 일관되게 유지하기 위해 5개 서비스 모두에 걸쳐 있는 분산 트랜잭션은 지원되지 않습니다.

대신 이 분산 트랜잭션을 프로그래밍 방식으로 구성해야 합니다.

분산 트랜잭션 지원을 추가하는 데 널리 사용되는 패턴은 Saga 패턴입니다. 로컬 트랜잭션을 프로그래밍 방식으로 그룹화하고 순차적으로 각 트랜잭션을 호출하여 구현됩니다. 로컬 트랜잭션 중 하나라도 실패하면 Saga는 작업을 중단하고 보상 트랜잭션 집합을 호출합니다. 보상 트랜잭션은 이전 로컬 트랜잭션의 변경 내용을 취소하고 데이터 일관성을 복원합니다. 그림 5-6은 Saga 패턴으로 실패한 트랜잭션을 보여 줍니다.

Roll back in saga pattern

그림 5-6. 트랜잭션 롤백

이전 그림에서는 인벤토리 마이크로 서비스에서 인벤토리 업데이트 작업이 실패했습니다. Saga는 재고 수를 조정하고, 결제 및 주문을 취소하고, 각 마이크로 서비스에 대한 데이터를 일관된 상태로 되돌리기 위해 일련의 보상 트랜잭션(빨간색)을 호출합니다.

Saga 패턴은 일반적으로 일련의 관련 이벤트로 구성되거나 관련 명령 집합으로 오케스트레이션됩니다. 4장에서는 오케스트레이션된 Saga 구현의 기초가 될 서비스 집계 패턴에 대해 설명했습니다. 또한 안무 사가 구현의 기초가 될 Azure Service BusAzure Event Grid 주제와 함께 이벤트에 대해 논의했습니다.

대용량 데이터

대규모 클라우드 네이티브 애플리케이션은 대용량 데이터 요구 사항을 지원하는 경우가 많습니다. 이러한 시나리오에서는 기존 데이터 스토리지 기술로 인해 병목 현상이 발생할 수 있습니다. 대규모로 배포되는 복잡한 시스템의 경우 CQRS(명령과 쿼리의 역할 분리) 및 이벤트 소싱 모두 애플리케이션 성능을 개선시킬 수 있습니다.

CQRS

CQRS는 성능, 확장성 및 보안을 최대화할 수 있도록 하는 아키텍처 패턴입니다. 이 패턴은 데이터를 읽는 작업과 데이터를 쓰는 작업을 구분합니다.

일반적인 시나리오의 경우 읽기 및 쓰기 작업 모두에 동일한 항목 모델 및 데이터 리포지토리 개체가 사용됩니다.

그러나 대용량 데이터 시나리오에서는 읽기 및 쓰기에 대해 별도의 모델과 데이터 테이블을 활용할 수 있습니다. 성능을 향상시키기 위해 읽기 작업은 데이터의 비정규화된 표현을 쿼리하여 비용이 많이 드는 반복적인 테이블 조인 및 테이블 잠금을 방지할 수 있습니다. 명령이라고 하는 쓰기 작업은 일관성을 보장하는 데이터의 완전히 정규화된 표현에 대해 업데이트됩니다. 그런 다음 두 표현을 동기화 상태로 유지하는 메커니즘을 구현해야 합니다. 일반적으로 쓰기 테이블이 수정될 때마다 수정 사항을 읽기 테이블에 복제하는 이벤트를 게시합니다.

그림 5-7은 CQRS 패턴의 구현을 보여 줍니다.

Command and Query Responsibility Segregation

그림 5-7. CQRS 구현

이전 그림에서는 별도의 명령 및 쿼리 모델이 구현되었습니다. 각 데이터 쓰기 작업은 쓰기 저장소에 저장된 다음 읽기 저장소로 전파됩니다. 데이터 전파 프로세스가 최종 일관성 원칙에 따라 작동하는 방식에 세심한 주의를 기울이세요. 읽기 모델은 결국 쓰기 모델과 동기화되지만 프로세스에 약간의 지연이 있을 수 있습니다. 다음 섹션에서 최종 일관성에 대해 설명합니다.

이러한 분리를 통해 읽기 및 쓰기를 독립적으로 조정할 수 있습니다. 읽기 작업은 쿼리에 최적화된 스키마를 사용하는 반면 쓰기는 업데이트에 최적화된 스키마를 사용합니다. 읽기 쿼리는 비정규화된 데이터에 대해 수행되는 반면 복잡한 비즈니스 논리는 쓰기 모델에 적용될 수 있습니다. 또한 읽기를 노출하는 작업보다 쓰기 작업에 더 엄격한 보안을 적용할 수 있습니다.

CQRS를 구현하면 클라우드 네이티브 서비스의 애플리케이션 성능을 개선시킬 수 있습니다. 그러나 결과적으로 더 복잡한 디자인이 됩니다. 이 원칙을 클라우드 네이티브 애플리케이션의 해당 섹션에 신중하고 전략적으로 적용하여 이점을 얻을 수 있습니다. CQRS에 대한 자세한 내용은 Microsoft 책 .NET 마이크로 서비스: 컨테이너화된 .NET 애플리케이션을 위한 아키텍처를 참조하세요.

이벤트 소싱

대량 데이터 시나리오를 최적화하는 또 다른 방법으로는 이벤트 소싱이 포함됩니다.

시스템은 일반적으로 데이터 엔터티의 현재 상태를 저장합니다. 예를 들어 사용자가 전화번호를 변경하면 고객 레코드가 새 번호로 업데이트됩니다. 데이터 엔터티의 현재 상태는 항상 알고 있지만 각 업데이트는 이전 상태를 덮어씁니다.

대부분의 경우 이 모델은 잘 작동합니다. 그러나 대용량 시스템에서는 트랜잭션 잠금 및 빈번한 업데이트 작업으로 인한 오버헤드가 데이터베이스 성능, 응답성에 영향을 미치고 확장성을 제한할 수 있습니다.

이벤트 소싱은 다른 데이터 캡처 방식을 사용합니다. 데이터에 영향을 미치는 각 작업은 이벤트 저장소에 유지됩니다. 데이터 레코드의 상태를 업데이트하는 대신 회계사의 원장과 유사한 과거 이벤트의 순차적 목록에 각 변경 내용을 추가합니다. 이벤트 저장소는 데이터에 대한 레코드 시스템이 됩니다. 마이크로 서비스의 경계 컨텍스트 내에서 다양한 구체화된 뷰를 전파하는 데 사용됩니다. 그림 5.8은 패턴을 보여 줍니다.

Event Sourcing

그림 5-8. 이벤트 소싱

이전 그림에서 사용자의 쇼핑 카트에 대한 각 항목(파란색)이 기본 이벤트 저장소에 어떻게 추가되는지 확인합니다. 인접한 구체화된 뷰에서 시스템은 각 쇼핑 카트와 관련된 모든 이벤트를 재생하여 현재 상태를 투영합니다. 그런 다음 이 보기 또는 읽기 모델은 UI에 다시 노출됩니다. 이벤트를 외부 시스템 및 애플리케이션과 통합하거나 쿼리하여 엔터티의 현재 상태를 확인할 수도 있습니다. 이 방법을 사용하여 기록을 유지 관리합니다. 엔터티의 현재 상태뿐만 아니라 이 상태에 도달한 방법도 알고 있습니다.

기계적으로 말하자면, 이벤트 소싱은 쓰기 모델을 간소화합니다. 업데이트나 삭제가 없습니다. 각 데이터 항목을 변경할 수 없는 이벤트로 추가하면 관계형 데이터베이스와 관련된 경합, 잠금 및 동시성 충돌이 최소화됩니다. 구체화된 뷰 패턴으로 읽기 모델을 빌드하면 쓰기 모델에서 뷰를 분리하고 애플리케이션 UI의 요구 사항을 최적화하기 위해 최상의 데이터 저장소를 선택할 수 있습니다.

이 패턴의 경우 이벤트 소싱을 직접 지원하는 데이터 저장소를 고려합니다. Azure Cosmos DB, MongoDB, Cassandra, CouchDB 및 RavenDB가 적합한 후보입니다.

모든 패턴 및 기술과 마찬가지로 필요할 때 전략적으로 구현합니다. 이벤트 소싱은 향상된 성능과 확장성을 제공할 수 있지만 복잡성과 학습 곡선을 희생해야 합니다.