소프트웨어 디자인은 진화하는 과정입니다. 모든 대형 시스템은 작은 시스템에서 시작됩니다. 기존 아키텍처에서 문제가 발생했지만 해결할 수 없으면 시스템이 진화하기 시작합니다. 모든 진화에는 몇 가지 기술적 선택이 수반됩니다. 어떤 문제를 해결해야 합니까? 어떤 대가를 치르게 될까요? 아키텍트나 선임 엔지니어는 개발 일정, 기술 스택 및 팀 수준에 관계없이 진화할 수 있는 합리적인 방법을 찾아야 하며 실행 가능한 솔루션을 만들기 전에 이러한 기준을 충족할 수 있어야 합니다.
이 기사에서는 CQRS
(명령 쿼리 책임 분할)의 정신과 해결해야 할 문제를 소개합니다. 우리는 작은 단일체에서 시작하여 모든 소프트웨어 시스템의 진화처럼 진화할 것이며, 이 기사에서는 각 진화의 이유와 접근 방식을 소개할 것입니다.
전통적인 모노리스
이것은 가장 일반적인 시스템 설계입니다. API 서버, 일반적으로 편안한 API 및 데이터베이스가 있습니다. 클라이언트는 백엔드와 미리 전송 형식을 협상합니다. 읽기와 쓰기 모두 데이터 전송 객체인 DTO를 통해 이루어집니다. 그러나 백엔드는 비즈니스 로직을 처리할 때 DTO를 도메인 지식이 있는 도메인 객체로 변환하고 도메인 객체를 데이터베이스의 저장 단위로 사용한다.
Read/Write Splitting 을 달성하기 위해 왼쪽의 쓰기 경로에서 클라이언트는 DTO를 백엔드로 보내 데이터베이스에 대한 CUD(생성/업데이트/삭제) 작업을 수행하고 백엔드 응답은 클라이언트에게 Ack for 처리 후 성공 및 실패에 대해 Nak . restful API에서 일반적으로 2xx는 성공을 나타내고 4xx는 실패를 나타냅니다. 오른쪽의 읽기 경로는 단순히 읽기 요청을 통해 해당 DTO를 가져옵니다.
클라이언트를 위한 DTO의 의미를 추가로 설명합니다. 클라이언트의 DTO에는 일반적으로 화면에 렌더링할 모든 데이터가 포함됩니다. 예를 들어 소셜 미디어에서 프로필을 보면 이름, 계정 및 기타 개인 정보는 물론 최근 활동, 팔로우한 활동까지 포함됩니다. DTO에는 이 페이지에 표시되어야 하는 모든 정보가 포함되어 있습니다.
읽기/쓰기 분할을 강조해야 하는 이유는 무엇입니까? 읽기 및 쓰기 경로 모두에서 동일한 절차를 사용할 수 없습니까? 우리는 미래에 시스템을 더 잘 최적화하기를 원하기 때문입니다. 쓰기 경로에는 특정 최적화 방법이 있으며 읽기 경로도 마찬가지입니다. 예를 들어 캐시를 만들기 위해 읽기 경로에서 읽기 캐싱을 사용하여 응답 시간을 줄일 수 있습니다. 그리고 write through caching을 통해 write path를 개선할 수 있습니다. 둘째, 쓰기를 비동기식으로 수행할 수도 있습니다. 모든 DTO는 메시지 대기열에 기록되고 작업자가 처리하여 방대한 양의 기록된 데이터를 처리합니다. 더욱이, 각각의 적절한 데이터베이스는 쓰기 및 읽기에 사용될 수 있습니다.
따라서 읽기/쓰기 분할이 필수적입니다. 그리고 시스템 설계의 초기 단계에서 고려되어야 합니다. 쓰기 경로는 데이터 지속성에 집중하는 것입니다. 반면 읽기 경로는 데이터 쿼리에 집중하는 것입니다.
그럼에도 불구하고 이 시스템 설계 모델에는 두 가지 주요 문제가 있습니다.
- 빈혈 모델. CRUD 모델이라고도 합니다. 백엔드가 데이터 변환에 초점을 맞추면 비즈니스 로직을 처리할 공간이 없어 비즈니스 로직이 도처에 흩어지게 됩니다. 도메인 지식도 사라질 것입니다. 예를 들어 경제 웹사이트에서는 "주문 기록 삽입" 대신 "구매"라고 말합니다.
- 불충분한 확장성. 시스템 아키텍처의 관점에서 데이터베이스는 전체 시스템의 병목 현상이 되기 쉽습니다. 읽기와 쓰기가 모두 있어야 합니다. RDBMS의 문제는 수평적 스케일링이 없기 때문에 더욱 심각합니다.
작업 기반 모노리스
위의 전통적인 모노리스가 직면한 문제를 해결하기 위해 여기에서 도메인 개념을 도입하려고 합니다.
이 다이어그램은 기본적으로 위의 다이어그램과 동일합니다. 유일한 차이점은 DTO를 쓰기 경로의 메시지로 바꾸는 것입니다. 메시지에는 DTO와 같은 데이터 자체뿐만 아니라 작업과 데이터가 포함됩니다. 따라서 백엔드가 각 작업을 더 쉽게 인식하고 해당 도메인 구현을 갖도록 메시지에 도메인별 작업을 수행할 수 있습니다.
이 단계에서 C in CQRS
이 나타나며 메시지는 일종의 명령입니다. 그러나 확장성 문제는 아직 해결되지 않았습니다.
또한 DTO를 단순화하고 메시지를 사용하여 통신하도록 변경했지만 여전히 읽기 경로에 DTO가 필요합니다. 소셜 미디어를 다시 예로 들어 보겠습니다. 닉네임 수정 시 메시지 형식은 {"rename": "LazyDr"}
. 그러나 프로필을 렌더링할 때 활동과 같은 추가 정보가 여전히 필요합니다. 이 정보 격차로 인해 DTO를 검색하기 위해 읽기 경로에서 많은 처리를 수행해야 합니다.
CQS(명령 쿼리 분할)
CQS의 등장은 위의 읽기/쓰기 분할의 문제점을 해결하기 위한 것입니다.
읽을 때 클라이언트는 DTO가 필요하므로 백엔드는 원래 도메인 개체에서 DTO를 미리 생성하고 읽기 전용 데이터베이스에 DTO를 저장하는 것과 같이 읽기 경로에서 읽기 전용 최적화를 수행할 수 있습니다.
이런 식으로 읽기 경로에서 애플리케이션 서비스의 구현이 더 간단해집니다. 애플리케이션 서비스는 페이징, 정렬 등만 담당하면 되는 씬 읽기 계층이 될 수 있습니다. 요청 후 클라이언트는 데이터베이스에서 DTO를 쉽게 검색할 수 있습니다.
따라서 문제는 누가 사전 구축된 DTO를 생성할 것인가 하는 것입니다. 쓰기 경로의 책임입니다.
다이어그램은 이전에 본 예와 유사하지만 실제로 도메인 개체를 유지하는 것 외에도 응용 프로그램 서비스도 DTO를 유지해야 합니다. 즉, 대부분의 비즈니스 로직은 쓰기 경로에서 눌려지고 다양한 읽기 보기가 준비되어야 합니다.
이 단계에서 우리는 도메인에서 발생하는 대부분의 문제를 해결했지만 확장에는 여전히 솔루션이 없습니다. 이제 스케일링을 더 정의합니다. 스케일링에는 두 가지 측면이 있습니다.
- 트래픽: 쓰기 볼륨이 증가합니다.
- 확장: 다양한 읽기 보기에 대한 필요성과 같은 기능 요구 사항이 증가합니다. 계속해서 소셜 미디어를 예로 들면 프로필에 하나의 프레젠테이션이 있지만 타임라인에 다른 프레젠테이션이 있을 수 있습니다.
CQRS
쓰기 경로가 읽기 보기 준비를 담당하는 이유는 무엇입니까? 쓰기는 지속성에 중점을 두어야 하며 이러한 다양한 읽기 보기는 쓰기 경로에서 처리되지 않아야 합니다. 그러나 읽기 경로에는 읽기만 있습니다. 누가 읽기 보기를 준비해야 합니까?
따라서 전체 솔루션은 다음과 같습니다.
왼쪽의 쓰기 경로와 오른쪽의 읽기 경로는 CQS 섹션에 도입되었습니다. 유일한 차이점은 쓰기 경로에 있는 데이터베이스를 읽기 경로에 사용되는 데이터베이스로 변환하는 역할을 하는 최종 블록이 추가된다는 것입니다. 데이터 동기화가 관련되면 데이터 일관성 문제가 발생할 수 있으므로 최종 일관성을 구현하기 위한 몇 가지 접근 방식을 짧은 시간에서 긴 시간 순으로 정렬하여 나열합니다.
- 배경 스레드: 대표적인 대표자는 Redis입니다. 데이터가 기본에 기록된 후 Redis는 즉시 데이터를 백그라운드의 복제본으로 보냅니다.
- 메시지 대기열과 작업자: 이는 비동기식 데이터 복제에 대한 일반적인 관행입니다. 데이터베이스에 쓸 때 이벤트가 메시지 대기열로 시작되고 작업자가 처리합니다.
- Extract-Transform-Load: 이 시간 간격은 몇 분에서 몇 시간에 이르는 가장 긴 시간입니다. map-reduce 또는 다른 방법을 사용하여 반대쪽에 결과를 기록합니다.
어떤 접근 방식이든 단일 소스 소스는 필수입니다. 즉, 변환에 오류가 발생하면 시스템이 완료되지 않은 작업을 복구할 수 있어야 합니다. 따라서 데이터는 고유하고 신뢰할 수 있어야 합니다.
데이터는 일반적으로 두 가지 유형,
- 상태: 상태는 은행 통장에 적힌 잔액과 같이 현재 보이는 것을 말합니다.
- 이벤트: 이벤트는 은행 통장의 모든 거래 기록과 같이 각 상태를 수정하는 작업입니다.
사실, 이벤트로 저장할 수 있는 메시지가 이미 있습니다. 쓰기 경로의 경우 메시지를 순서대로 저장하는 것이 매우 효율적입니다. 각각의 다른 메시지를 통해 필요에 따라 다른 읽기 보기를 쉽게 구축할 수 있습니다. 이 접근 방식을 이벤트 소싱 이라고도 합니다.
하지만 이벤트만 효율적으로 사용하기 어렵습니다. 최종 결과를 얻으려면 모든 변환을 처음부터 끝까지 실행하여 읽기 보기를 다시 작성해야 합니다. 결과적으로 하이브리드 방법이 이상적입니다. 쓰기 경로에는 상태와 이벤트가 모두 유지되며 변환 프로세스는 실제 상황에 따라 데이터 소스를 선택할 수 있습니다.
CQRS에서 데이터의 전체 수명 주기를 요약합니다.
데이터는 클라이언트에서 시작한 다음 명령 형식으로 백엔드에 입력됩니다. 비즈니스 로직에 따라 도메인 객체로 변환되어 데이터베이스에 저장됩니다. 이러한 도메인 개체는 다양한 읽기 보기로 변환되고 요구 사항에 따라 다른 읽기 특수 데이터베이스에 저장됩니다. 마지막으로 클라이언트는 이러한 읽기 보기를 DTO 형식으로 다시 가져옵니다.
결론
많은 패턴으로 DDD와 CQRS를 설명하는 많은 책과 기사가 있습니다. 내 관점에서 이러한 패턴은 Entity, Value Object, Aggregate 등과 같은 DDD의 상상력에 한계를 초래합니다. 결과적으로 대부분의 개발자는 DDD가 자신과 거리가 멀고 구현뿐만 아니라 구현하기 어렵다고 느낍니다. 사실, DDD의 개념은 그렇게 복잡하지 않습니다. 대신, DDD는 비즈니스 로직을 캡슐화한 다음 기능 요구 사항 확장을 용이하게 하기 위해 제안됩니다.
CQRS는 훨씬 더 간단합니다. 이 글에서는 시스템 진화의 과정에서 시작하여 전체 시스템 설계 과정과 해결해야 할 문제를 이해하고 마침내 CQRS의 결론을 자연스럽게 도출합니다.
시스템 설계에는 은총알이 없습니다. 각각의 진화는 특정한 문제를 해결하기 위해 만들어지지만 새로운 문제가 나올 수 있습니다. 이 기사의 설계 프로세스를 예로 들어, CQRS는 언급된 모든 문제, 빈약한 모델 및 불충분한 확장성을 해결하는 것처럼 보이지만 실제로 CQRS는 데이터 일관성과 같은 새로운 문제도 가져옵니다. 각 기술 선택에는 장단점이 있습니다. 모든 옵션 뒤에 있는 모든 위협을 이해하는 한 상대적으로 수용 가능한 접근 방식을 선택할 수 있습니다.
CQRS를 선택하더라도 실제로는 최종 일관성을 구현하기 위한 세 가지 선택 사항이 있습니다. 시스템 설계는 지속적인 선택의 결과입니다.
이 기사의 목적은 DDD가 그렇게 무섭지 않고 CQRS가 그렇게 복잡하지도 않고 단지 결정일 뿐이라는 것을 알려주는 것입니다.
https://medium.com/interviewnoodle/shift-from-monolith-to-cqrs-a34bab75617e
[광고] STEEM 개발자 커뮤니티에 참여 하시면, 다양한 혜택을 받을 수 있습니다.
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit