Go에서 CQRS 구현: 확장 가능한 아키텍처를 위한 실용적 가이드

불필요한 형식주의 없이 Go로 CQRS 구축하기

Page content

CQRS는 과대평가되고, 지나치게 복잡해지며, 때로는 평범한 CRUD의 지루함을 해결할 만병통치약으로 오해받는 패턴 중 하나입니다.

유용한 버전은 훨씬 더 단순합니다. 상태를 변경하는 코드와 상태를 읽는 코드를 분리한 후, 각 측면이 자신의 역할에 맞게 진화하도록 허용합니다. 마틴 파울러(Martin Fowler)는 CQRS를 정보를 업데이트하는 데 사용되는 모델과 읽는 데 사용되는 모델을 다르게 사용하는 것으로 설명하면서도, 대부분의 시스템에서는 위험한 복잡성을 추가한다고 경고합니다. 마이크로소프트(Microsoft)도 더 운영 중심적인 용어로 동일한 핵심 포인트를 제시합니다. 즉, 읽기와 쓰기 모델을 분리하여 각 모델을 독립적으로 최적화할 수 있게 하는 것입니다.

CQRS in Go — commands and queries as separate paths through a Go gopher hub

Go로 작업한다면, 이 아이디어는 해당 언어에 매우 잘 부합합니다. Go는 명확한 경계, 작은 인터페이스, 심심할 정도로 단순한 데이터 타입, 사용 사례 중심의 패키지를 잘 다룹니다. 이로 인해 Go에서의 기본 CQRS는 컨퍼런스 슬라이드에서 종종 보이는 것보다 훨씬 덜 극적입니다. 시작하기 위해 이벤트 소싱(Event Sourcing), 카프카(Kafka), 세 개의 데이터베이스가 필요하지 않습니다. 사실, 마이크로소프트의 CQRS 가이드와 Three Dots Labs의 Go 예제는 간단한 구현이 동일한 기반 저장소를 공유할 수 있으며, 명령(Command) 및 쿼리(Query) 핸들러를 먼저 추가하고 실제 문제 발생 시에만 더 복잡한 인프라를 도입할 수 있음을 보여줍니다.

CQRS의 실제 의미

핵심적으로 CQRS는 명령(Command)과 쿼리(Query) 사이에 명확한 경선을 그립니다. 쿼리는 데이터를 읽고 시스템 상태를 수정해서는 안 됩니다. 명령은 상태를 변경하며 도메인 데이터를 주요 결과로 반환해서는 안 됩니다. Three Dots Labs는 이를 실용적인 Go 용어로 표현합니다. 즉, 쿼리는 데이터를 반환하고 명령은 변경 사항을 수행하며, 오류는 명령의 정상적인 결과라는 것입니다. 이것이 기본 동작입니다. 나머지는 모두 선택 사항입니다.

흔히 있는 오해는 CQRS가 자동으로 별도의 데이터베이스, 비동기 투영(Projection), 또는 이벤트 소싱을 의미한다는 것입니다. 이는 사실이 아닙니다. 마이크로소프트의 패턴 가이드는 별도의 데이터 저장소를 기본이 아닌 더 고급 형태라고 명시적으로 취급하며, Three Dots Labs는 주어진 시스템에 충분하기 때문에 쿼리가 쓰기 작업과 동일한 데이터베이스에서 읽는 Go 구현을 보여줍니다. 만약 당신의 글이 한 가지 것만 명확히 가르친다면, 그것은 이 되어야 합니다: CQRS는 주로 모델링 및 애플리케이션 구조의 선택이며, 필수적인 분산 시스템 패키지 번undles이 아닙니다.

또 다른 중요한 세부 사항은 명명법입니다. 명령은 비즈니스 의도를 모델링해야 하며, 저장소 뮤테이션(Storage Mutations)을 모델링해서는 안 됩니다. 마이크로소프트의 예시는 “호텔 객실 예약"과 “예약 상태를 ‘예약됨’으로 설정"을 대조하며, Three Dots Labs는 도메인 전문가들이 말하는 방식에 가까운 이름인 “ScheduleTraining” 또는 “CancelTraining"과 같이 일반적인 “Create” 및 “Delete” 동사 대신 사용하는 것을 권장합니다. Go에서 이러한 명명 규율은 명령 이름이 종종 타입 이름, 핸들러 이름, 패키지 경계가 되기 때문에 그 가치가 드러납니다.

팀들이 CQRS를 선택하는 이유

단일 CRUD 모델이 너무 많은 역할을 제대로 수행하지 못할 때 CQRS는 매력적으로 다가옵니다. 마이크로소프트의 가이드는 일반적인 압박 포인트를 나열합니다. 동일한 데이터의 읽기 및 쓰기 표현이 달라지고, 동시 업데이트가 잠금 경쟁(Lock Contention)을 유발하며, 읽기 성능이 쿼리 복잡성으로 인해 저하되고, 공유 엔티티가 보안 규칙을 엉키게 만듭니다.换句话说, 문제는 CRUD가 도덕적으로 잘못되었다는 것이 아닙니다. 문제는 하나의 모델이 서로 충돌하는 요구사항을 동시에 만족시키도록 강요받고 있다는 것입니다.

이는 기술 제품에서 특히 흔합니다. 쓰기 작업은 검증, 불변식, 트랜잭션, 비즈니스 규칙에 관심을 가집니다. 반면 읽기 작업은 필터, 조인(Joins), 집계, 캐싱, 정렬, 그리고 페이지나 API가 필요로 하는 정확한 형태를 제공하는 데 관심을 가집니다. CQRS는 쓰기 측면이 엄격하고 도메인 중심이 되도록 허용하면서, 읽기 측면은 실용적이고 DTO 중심이 있도록 합니다. 마이크로소프트는 검증과 일관성에 초점을 맞춘 쓰기 모델과, 표현 및 응답성을 위해 최적화된 DTO 또는 투영에 초점을 맞춘 읽기 모델을 명시적으로 권장합니다.

또한 팀 수준의 이점도 있습니다. Three Dots Labs는 명령과 쿼리를 분리하면 결합이 느슨해지고, 실행 흐름이 명확해지며, 개발자가 무작정 서비스 레이어의 로직을 추적하는 대신 사용 가능한 명령 및 쿼리의 작은 목록을 검사할 수 있어 온보딩이 빨라진다고 주장합니다. 마이크로소프트도 여러 사용자가 동일한 데이터를 업데이트하고 명령이 충돌을 방지하거나 해결할 만큼 충분한 세분성을 필요로 하는 협업 환경에서 CQRS가 특히 유용함을 지적합니다.

제 개인적인 견해는 다음과 같습니다. 대부분의 팀은 하나의 “서비스"가 이미 중구난방의 모노리스로 변한 후에야 CQRS를 채택합니다. 하지만 많은 팀이 아키텍처 다이어그램이 비싸고 진지해 보였기 때문에 너무 일찍 채택하기도 합니다. 올바른 시기는 읽기와 쓰기가 형태, 속도, 규칙에서 명확하게 벗어나 있을 때이며, 당신의 Todo 앱이 야망이 생겼을 때가 아닙니다.

이점과 대가

별도의 저장소나 메시징을 추가하기 전에도 기본 CQRS는 실제 이점을 제공합니다. 더 작은 명령 모델, 더 작은 쿼리 모델, 더 명확한 사용 사례, 그리고 로깅 및 계측(Instrumentation)과 같은 횡단면 관심사(Cross-cutting Concerns)를 적용할 더 명확한 장소를 제공합니다. Three Dots Labs는 더 나은 코드 조직화, 느슨한 결합, 단순한 모델을 즉각적인 성과로 꼽으며, Microservices.io는 단순화된 명령 및 쿼리 모델과 비정규화(De-normalized)된 확장 가능한 읽기 뷰 지원을 강조합니다.

문제가 이를 정당화할 때, CQRS는 더 강력한 읽기 측 최적화의 문을 엽니다. 마이크로소프트의 가이드는 별도의 읽기 모델이 DTO, 투영, 읽기 전용 복제본, 또는 완전히 다른 저장 기술을 사용할 수 있음을 지적합니다. 또한 무거운 조인과 ORM 중심 쿼리 경로를 피하기 위한 방법으로 물질화 뷰(Materialized Views)를 언급합니다. 쓰기 측에서 사용할 데이터 액세스 레이어를 평가 중이라면, PostgreSQL용 Go ORM 비교는 GORM, Ent, Bun, sqlc 간의 트레이드오프를 실용적으로 다룹니다. 여기서 CQRS가 구조적으로뿐만 아니라 운영적으로도 그 가치를 발휘하기 시작합니다.

비용은 마찬가지로 현실적입니다. 파울러의 경고가 여전히 올바른 출발점입니다. 대부분의 시스템에서 CQRS는 위험한 복잡성을 추가합니다. 마이크로소프트는 증가된 복잡성과 최종 일관성을 핵심 고려 사항으로 나열하며, Microservices.io는 읽기 뷰에서 잠재적인 코드 중복 및 복제 지연을 추가합니다. 저장소를 분리하면, 데이터베이스와 브로커 간의 깔끔한 분산 트랜잭션에 의존하지 않고 일반적으로 이벤트를 통해 저장소를 동기화하는 작업을 상속받게 됩니다.

이벤트 소싱은 이러한 대가를 없애지 않습니다. 단지 그 형태를 바꿀 뿐입니다. 마이크로소프트의 CQRS 가이드는 이벤트 소싱이 이벤트 저장소를 단일 진실 공급원(Single Source of Truth)으로 만들고, 역사를 재생하여 물질화 뷰를 재구성할 수 있게 한다고 말합니다. 반면 Event Horizon은 추적 가능성과 감사 로깅을 주요 이점으로 꼽습니다. 그러나 마이크로소프트는 뷰 생성, 재생, 이벤트 처리가 더 많은 설계 복잡성을 추가한다고 경고하며 재생 비용을 줄이기 위해 스냅샷을建议使用합니다. 그래서 저는 이벤트 소싱을 “진입 티켓"이 아닌 “CQRS에 두 번째 어려운 결정"으로 설명하는 것을 선호합니다.

기억해둘 가치가 있는 유용한 경험 법칙은 기본 CQRS는 저렴하지만 분산 CQRS는 비싸며, 이 두 대화를 혼동하는 것이 팀들이 문제에게 필요한 것보다 훨씬 더 많은 복잡성을 얻는 가장 흔한 방법 중 하나라는 것입니다.

Go에서 간단한 CQRS 구현

Go에서 합리적인 첫 단계는 하나의 데이터베이스를 유지하고 애플리케이션 레이어만 분리하는 것입니다. 명령은 비즈니스 규칙과 지속성을 소유합니다. 쿼리는 호출자를 위해 형성된 읽기 모델을 반환합니다. 이는 Three Dots Labs가 비동기 버스나 별도 읽기 저장소를 사용하기 전에 권장하는 바로 그 종류의 기본 CQRS입니다.

명령으로 시작

package blog

import (
	"context"
	"errors"
	"time"
)

type PublishPostCommand struct {
	Title   string
	Slug    string
	BodyMD  string
	Author  string
}

type PostRepository interface {
	NextID(ctx context.Context) (string, error)
	Save(ctx context.Context, post Post) error
}

type Post struct {
	ID          string
	Title       string
	Slug        string
	BodyMD      string
	Author      string
	PublishedAt time.Time
}

type PublishPostHandler struct {
	Repo  PostRepository
	Now   func() time.Time
}

func (h PublishPostHandler) Handle(ctx context.Context, cmd PublishPostCommand) error {
	if cmd.Title == "" || cmd.Slug == "" || cmd.BodyMD == "" {
		return errors.New("title, slug, and body are required")
	}

	id, err := h.Repo.NextID(ctx)
	if err != nil {
		return err
	}

	post := Post{
		ID:          id,
		Title:       cmd.Title,
		Slug:        cmd.Slug,
		BodyMD:      cmd.BodyMD,
		Author:      cmd.Author,
		PublishedAt: h.Now(),
	}

	return h.Repo.Save(ctx, post)
}

이 핸들러는 페이지를 제공하거나 목록 응답을 형성하거나 카드 그리드를 위한 SQL을 최적화하려고 하지 않습니다. 단지 의도를 강제하고 유효한 애그리거트를 지속시킬 뿐입니다. 이것이 명령 측이 하나의 작업을 잘 수행하는 방식입니다.

쿼리 추가

package blog

import "context"

type PostView struct {
	ID          string
	Title       string
	Slug        string
	Author      string
	PublishedAt string
	Excerpt     string
}

type LatestPostsQuery struct {
	Limit int
}

type PostReadModel interface {
	Latest(ctx context.Context, limit int) ([]PostView, error)
	BySlug(ctx context.Context, slug string) (PostView, error)
}

type LatestPostsHandler struct {
	ReadModel PostReadModel
}

func (h LatestPostsHandler) Handle(ctx context.Context, q LatestPostsQuery) ([]PostView, error) {
	limit := q.Limit
	if limit <= 0 {
		limit = 10
	}
	return h.ReadModel.Latest(ctx, limit)
}

type GetPostBySlugQuery struct {
	Slug string
}

type GetPostBySlugHandler struct {
	ReadModel PostReadModel
}

func (h GetPostBySlugHandler) Handle(ctx context.Context, q GetPostBySlugQuery) (PostView, error) {
	return h.ReadModel.BySlug(ctx, q.Slug)
}

읽기 측이 쓰기 모델이 아닌 PostView를 반환하는 것에 주목하십시오. 이는 쓰기 모델이 트랜잭션 무결성과 도메인 규칙을 위해 튜닝되는 동안, 읽기 모델이 DTO 및 표현을 위해 최적화되어야 한다는 마이크로소프트의 권장 사항을 반영합니다.

신전이 아닌 Go 애플리케이션처럼 연결

package app

import "your/module/internal/blog"

type Application struct {
	Commands Commands
	Queries  Queries
}

type Commands struct {
	PublishPost blog.PublishPostHandler
}

type Queries struct {
	LatestPosts   blog.LatestPostsHandler
	GetPostBySlug blog.GetPostBySlugHandler
}

이 형태는 우연이 아닙니다. Three Dots Labs는 Wild Workouts에서 매우 유사한 패턴을 사용합니다. CommandsQueries를 노출하는 Application 타입, 그리고 구체적인 핸들러가 별도의 app/commandapp/query 패키지에서 연결됩니다. 그들의 서비스 구성 코드는 이러한 패키지를 별도로 가져와 단일 애플리케이션 객체를 구성합니다. 이는 프레임워크 드라마(Framework Drama) 없이 경계를 명확히 하는 깔끔하고 Go다운 방식입니다. 핸들러가 증가함에 따라 의존성 그래프가 복잡해지면, Go에서의 의존성 주입은 Wire, Dig 및 생성자 주입 패턴을 다루며 이 핸들러 기반 구조와 자연스럽게 결합합니다.

나중에 비동기 명령, 서비스 간 이벤트, 또는 비정규화된 검색 인덱스가 필요하면 이 기준선에서 추가할 수 있습니다. Three Dots Labs는 비동기 명령 버스 및 별도 쿼리 데이터베이스를 시작점이 아닌 후속 최적화로 명시적으로 제시합니다.

알아두어야 할 Go 라이브러리

Go CQRS 생태계는 .NET 생태계보다 좁으며, 솔직히 그것은 축복입니다. 오후 몇 시간 만에 실제 옵션을 조사하고 필요하지 않은 세 가지 추상화를 채택하는 것을 피할 수 있습니다.

Watermill

Watermill은 CQRS와 메시징을 원할 때 가장 명확한 현대적인 선택입니다. 그 CQRS 구성 요소는 원시 메시지가 아닌 Go 구조체로 작업할 수 있게 해주는 고수준 API이며, 그 빌딩 블록에는 EventBus, EventProcessor, CommandBus, CommandProcessor가 포함됩니다. 문서에는 공유 토픽의 순차적 처리를 위한 이벤트 핸들러 그룹, 읽기 모델 예제, 사용자 정의 마샬링 메타데이터도 다룹니다. CQRS 레이어 외부에서 Watermill은 RabbitMQ, Kafka, NATS Jetstream, Redis Streams, Google Cloud Pub/Sub, SQL, HTTP 등 다양한 퍼블리시/서브스크라이브(Pub/Sub) 백엔드를 지원합니다. Pkg.go.dev는 Watermill을 v1.0.0 이후 안정적인 공개 API로 프로덕션 준비가 된 것으로 표시하며, 현재 게시된 모듈 버전은 v1.5.2이며 GitHub에서 해당 릴리스가 5월 13일에 나열되어 있습니다.

commandBus, err := cqrs.NewCommandBusWithConfig(pub, cfg)
eventBus, err := cqrs.NewEventBusWithConfig(pub, cfg)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cfg)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cfg)

명령과 이벤트가 프로세스 경계를 넘어야 하거나, 재시도 및 재전송 시맨틱스를 일류 시민으로 원하거나, 당신의 “단순한” 서비스가 이미 이벤트 중심 현실로 halfway 갔음을 알고 있을 때 Watermill을 사용하십시오. 단점은 이제 원했든与否하든 브로커, 토픽, 순서, 그리고 멱등성에 대한 대화를 하게 된다는 것입니다. 이는 Watermill의 결함이 아닙니다. 이는 문제 공간의 비용입니다.

Event Horizon

Event Horizon은 Go용 CQRS 및 이벤트 소싱 툴킷입니다. 유지자들은 이를 프로덕션 시스템에서 사용된다고 묘사하지만, API가 최종적이지 않음을 또한 지적합니다. 이 툴킷은 애그리거트, 명령, 이벤트 등록 헬퍼, 메모리 및 MongoDB 변형용 공식 이벤트 저장소 구현, 투영 및 저장소 지원, 그리고 아웃박스 패턴 기반 애플리케이션을 포함한 예제를 제공합니다. 릴리스 스트림은 여전히 활성 상태이며, GitHub는 6월 16일 v0.17.0을 보여주며 이전 릴리스는 스냅샷, 재시도 가능한 투영, 지속적 명령 예약, 아웃박스 패턴과 같은 기능을 추가했습니다.

eh.RegisterAggregate(func(id uuid.UUID) eh.Aggregate {
	return &InvoiceAggregate{ID: id}
})

eh.RegisterCommand(func() eh.Command {
	return &CreateInvoiceCommand{}
})

이벤트 소싱이 지점이거나 선택적 미래 확장이 아닌 경우 Event Horizon이 가장 합리적입니다. 감사 친화적 스트림, 재생 가능한 역사, 투영, 이벤트 저장소 중심 모델을 원한다면 심각한 옵션입니다. 모노리ث에서 더 깨끗한 애플리케이션 서비스만 원한다면, 필요한 것보다 더 많은 기계 장치일 가능성이 높습니다. “API가 최종적이지 않음"이라는 참고는 또한 Watermill보다 시간이 지나며 더 많은 적응을 예산에 포함해야 함을 의미합니다.

Go-MediatR

Go-MediatR은 전체 CQRS 프레임워크가 아니지만, 프로세스 내(In-process) CQRS에 유용합니다. README는 이를 CQRS와 함께 사용되는 미디어터 패턴 구현으로 묘사하며, 명령 및 쿼리의 요청/응답 디스패치, 이벤트의 알림 디스패치, 횡단면 관심사를 위한 파이프라인 동작을 제공합니다. 프로젝트는 또한 태그된 릴리스를 가지고 있으며, GitHub는 v1.4.0을 최신 릴리스로 나열하고 스레드 안전 핸들러 등록 및 동시성 관련 개선 사항을 강조합니다.

resp, err := mediatr.Send[*CreateProductCommand, *CreateProductResponse](ctx, cmd)
post, err := mediatr.Send[*GetPostBySlugQuery, *PostView](ctx, query)

핸들러 기반 명령과 쿼리를 원하지만 브로커, 투영 엔진, 또는 이벤트 저장소를 원하지 않는다면 이는 좋은 적합성입니다. .NET의 MediatR에서 온 팀에게 특히 친숙합니다. 트레이드오프는 마찬가지로 명확합니다: 여전히 자신의 지속성, 읽기 모델 갱신 전략, 프로세스 외 통합 스토리를 설계해야 합니다. 즉, 애플리케이션 경계는 제공하지만 전체 아키텍처는 아닙니다.

오래된 프레임워크 및 참고 자료

여전히 교훈적인 오래된 Go CQRS 라이브러리가 있지만, 저는 이를 신규 프로덕션 채택의 기본값으로 다루기 전에 참고 자료로 취급할 것입니다.

jetbasrawi/go.cqrs는 Greg Young의 원칙을 기반으로 한 샘플 애플리케이션을 갖춘 Go CQRS 참조 구현으로 자신을 묘사합니다. 그러나 pkg.go.dev는 유효한 go.mod, 태그된 버전, 안정된 버전이 없음을 보여주며, GitHub는 릴리스가 없고 패키지 메타데이터가 7.4년 전에 게시되었음을 보여줍니다. 이는 유용한 역사이지만, 2026년의 새로운 프로덕션 채택을 위한 강력한 신호는 아닙니다.

andrewwebber/cqrs도 유사합니다: 이벤트 소싱, 명령 발급 및 처리, 이벤트 게시, 게시된 이벤트로부터의 읽기 모델 생성을 제공하지만, 패키지 메타데이터도 7.4년 전에 게시되었습니다. 이전 Go CQRS 라이브러리가 문제를 어떻게 접근했는지 이해하고 싶다면 반드시 읽을 것입니다. 그러나 자신의 아키텍처 스택의 파트타임 유지자가 되는 것에 만족하지 않는 한, 새로운 코드베이스의 기반으로 삼는 것을 조심할 것입니다.

실용적인 Go 프로젝트 레이아웃

일반적인 Go CQRS 레이아웃은 사용 사례를 명확히 해야 하며, 일반화된 추상화 아래에 묻어서는 안 됩니다. Wild Workouts은 여기서 좋은 참고 자료입니다. 리포지토리는 internal 아래에 바운디드 컨텍스트를 분리하고, 명령과 쿼리를 별도의 애플리케이션 패키지에 유지하며, CommandsQueries를 노출하는 Application 타입에 연결합니다. 서비스 구성은 어댑터, 핸들러, 의존성을 명시적으로 모읍니다. 여기에 설명된 패턴은 Go 프로젝트 구조: 관행 및 패턴의 더 넓은 가이드와 일치하며, 이는 Go 코드베이스가 성장함에 따라 팀이 직면하는 레이아웃 결정의 더 넓은 세트를 다룹니다.

실용적인 레이아웃은 다음과 같습니다:

internal/
  blog/
    app/
      app.go
      command/
        publish_post.go
        unpublish_post.go
      query/
        get_post_by_slug.go
        latest_posts.go
    domain/
      post.go
      slug.go
    adapters/
      postgres/
        post_repository.go
        post_read_model.go
    ports/
      http/
        handler.go
    service/
      application.go

이 레이아웃은 몇 가지 장점이 있습니다.

첫째, 명령 및 쿼리 핸들러는 구현하는 사용 사례에 가깝게 위치합니다. 이는 비즈니스 동작을 저장소나 전송 레이어 이름을 딴 핸들러에 숨기는 것을 더 어렵게 만듭니다. Three Dots Labs는 Wild Workouts에서 이를 직접 수행하며, app/commandapp/query가 별도의 패키지이고 최상위 Application이 책임을 따라 핸들러를 그룹화합니다.

둘째, 도메인 패키지는 불변식과 동작에 집중할 수 있으며, 읽기 측은 DTO와 투영을 반환하는 데 자유롭습니다. 이는 마이크로소프트의 쓰기 모델 및 읽기 모델 가이드와 일치하며, 읽기 측이 이념적 순수성 때문에 도메인 객체로 다시 강제로 통과되는 일반적인 CQRS 안티패턴을 피합니다.

셋째, 이 구조는 가장 작은 유용한 CQRS에서 더 무거운 변형까지 확장됩니다. 오늘 하나의 PostgreSQL 데이터베이스와 두 개의 저장소 구현을 유지하고, 나중에 검색 인덱스 또는 이벤트 중심 읽기 투영을 추가하여 전체 애플리케이션 형태를 다시 작성하지 않아도 됩니다. Three Dots Labs는 시스템이 필요할 때만 기본 CQRS에서 비동기 명령 버스 및 별도 쿼리 저장소로 진행하는 것을 명시적으로 설명합니다.

CQRS가 적합할 때와 적합하지 않을 때

읽기와 쓰기가 진정으로 다른 문제일 때 CQRS는 의미가 있습니다. 마이크로소프트는 읽기와 쓰기 모델이 독립적인 최적화가 필요하거나, 여러 사용자가 동일한 데이터를 협업하거나, 명확한 분리가 성능, 확장성, 보안을 돕는 워크로드를 위해 이를 권장합니다. Microservices.io는 도메인 이벤트 또는 물질화 투영에서 구축된 비정규화, 고성능 뷰를 또 다른 전형적인 적합성으로 추가합니다. Three Dots Labs도 복잡한 비즈니스 로직, 유지 관리 용이성, 비동기 명령 또는 전문 읽기 저장소로의 미래 확장을 강력한 채택 이유로 꼽습니다.

실무에서, 이는 종종 풍부한 도메인 규칙, 비용이 많이 드는 읽기 모델, 애그리거트에 깔끔하게 매핑되지 않는 보고 뷰, 또는 이벤트를 게시하고 다른 곳에서 투영을 구축하는 마이크로서비스가 있는 시스템을 의미합니다. 이러한 맥락에서, 분산 트랜잭션을 위한 Saga 패턴은 종종 서비스 경계를 넘나드는 다단계 비즈니스 작업의 조정 메커니즘으로 CQRS와 함께 나타납니다. 또한 쓰기 측이 엄격하고 감사 가능해야 하며 읽기 측이 빠르고 UI 또는 API 소비를 위해 형성되어야 하는 제품에도 적합합니다. 이미 투영, 복제본, 또는 이벤트로부터 뷰 재구성에 대해 이야기하고 있다면, 라벨을 사용하든 아니든 CQRS 영역에 있는 것입니다.

서비스가 직관적인 데이터 편집기일 때 CQRS는 의미가 없습니다. 파울러는 대부분의 시스템에서 CQRS가 위험한 복잡성을 추가한다고 단호하게 말하며, Three Dots Labs는 본질적으로 동일한 데이터를 수신하고 반환하는 단순한 CRUD 서비스는 좋은 적합성이 아님을 말합니다. 그들의 Wild Workouts 예시에서, 더 단순한 사용자 서비스는 Clean Architecture와 CQRS를 사용하지 않습니다. 왜냐하면 패턴들이 그곳에서 그 대가를 치르지 않기 때문입니다.

기술 블로그에서 plainly 말할 가치가 있는 부분은 이것입니다: CQRS는 성숙도 배지가 아니라 의도적인 트레이드오프이며, 실제로 그것이 제공하는 것이 필요할 때만 의미가 있습니다. 관리자 패널이 행을 쓰고 동일한 행을 다시 읽는다면, 단지 할 수 있기 때문에 모델을 분리하지 마십시오. 명령 핸들러가 대부분 “기록 Y의 필드 X 설정"이라면, CQRS 문제가 있는 것이 아닙니다. 당신은 정상적인 애플리케이션을 가지고 있으며, 그것은 완벽하게 존경할 만한 소프트웨어입니다.

마무리 생각

Go에서 CQRS를 구현하는 최선의 방법은 지루한 버전으로 시작하는 것입니다. 명령 핸들러를 쿼리 핸들러와 분리하십시오. 명령이 비즈니스 의도를 모델링하도록 하십시오. 쿼리가 읽기 모델을 반환하도록 하십시오. 필요한 것이 모두라면 동일한 데이터베이스를 유지하십시오. 그런 다음, 시스템이 손을 강제로 잡을 때만 비동기 버스, 투영, 별도 저장소, 또는 이벤트 소싱을 추가하십시오. 이 진행 과정은 복잡성에 대한 파울러의 경고, 마이크로소프트의 단계적 CQRS 가이드, 그리고 Three Dots Labs의 실용적인 Go 예시와 일치합니다.

라이브러리가 필요하다면, Watermill은 Go에서 메시지 중심 CQRS를 위한 가장 강력한 범용 선택이며, Event Horizon은 이벤트 소싱이 중력 중심일 때 매력적이고, Go-MediatR은 프로세스 내 명령 및 쿼리 디스패치만 필요할 때 좋은 가벼운 터치입니다. 다른 모든 것은 그 자리를 매우 신중하게 획득해야 합니다. 프로덕션 Go 시스템의 코드 구조, 통합, 데이터 액세스 패턴에 대한 더 넓은 지도가 필요하다면, App Architecture 가이드는 유용한 동반자입니다.

결국, 이것이 CQRS에 대한 가장 Go다운 답변입니다: 패턴을 사용하되, 의상은 사용하지 마십시오.

구독하기

시스템, 인프라, AI 엔지니어링에 관한 새 글을 받아보세요.