CloudNative
CQRS와 읽기 · 쓰기 · 책임의 분리
CQRS의 개념과 기존 데이터 모델의 문제점, Command와 Query 분리 구조 및 적용 기준
1 ) CQRS
CQRS란
CQRS(Command Query Responsibility Segregation)는 데이터를 변경하는 작업과 조회하는 작업의 책임을 분리하는 패턴이다.
여기서 애플리케이션의 요청을 크게 두 가지로 구분한다.
| 구분 | 역할 |
|---|---|
| Command | 데이터의 상태를 변경한다. |
| Query | 데이터를 조회한다. |
즉, 하나의 모델에서 데이터의 조회와 변경을 모두 담당하는 대신 쓰기(Command)와 읽기(Query)를 서로 다른 책임으로 분리한다.
┌→ Command → 데이터 변경
Client ───────────┤
└→ Query → 데이터 조회
CQRS를 적용하면 읽기와 쓰기를 각각의 요구사항에 맞게 구성할 수 있으므로 성능, 확장성, 보안 등의 측면에서 서로 다른 전략을 적용할 수 있다.
전통적인 구조의 문제점
일반적인 CRUD 애플리케이션에서는 하나의 데이터 모델을 이용하여 데이터의 생성, 조회, 수정, 삭제를 모두 처리할 수 있다.
하나의 Model
│
┌────────────┼────────────┐
↓ ↓ ↓
Create Read Update / Delete
│
Database
단순한 애플리케이션에서는 이러한 구조가 오히려 이해하고 구현하기 쉽다.
하지만 애플리케이션과 비즈니스 로직이 복잡해지면 읽기와 쓰기가 하나의 모델에 서로 다른 요구사항을 요구하는 문제가 발생할 수 있다.
읽기와 쓰기가 요구하는 형태의 차이
데이터를 조회할 때는 사용자에게 필요한 형태에 따라 서로 다른 DTO(Data Transfer Object)를 반환해야 할 수 있다.
반면 데이터를 생성하거나 수정할 때는 단순한 데이터 저장뿐 아니라 다음과 같은 처리가 필요할 수 있다.
입력
↓
유효성 검사
↓
비즈니스 로직
↓
데이터 변경
하나의 모델이 이러한 읽기와 쓰기의 요구사항을 모두 처리하기 시작하면 모델의 역할과 구조가 복잡해질 수 있다.
읽기와 쓰기의 부하 차이
일반적으로 읽기와 쓰기의 요청량이나 필요한 성능이 항상 동일하지는 않다.
예를 들어 조회 요청이 쓰기 요청보다 훨씬 많은 서비스라면 읽기 영역에 더 많은 자원이 필요할 수 있다.
하지만 읽기와 쓰기가 하나의 구조에 강하게 묶여 있으면 각각을 독립적으로 최적화하거나 확장하기 어려울 수 있다.
불필요한 데이터 처리
읽기와 쓰기에서 사용하는 데이터 표현이 서로 일치하지 않을 수도 있다.
하나의 모델을 공통으로 사용하면 특정 작업에는 필요하지 않은 Column이나 속성까지 함께 처리해야 하는 상황이 발생할 수 있다.
데이터 경합
여러 사용자가 동일한 데이터에 대해 병렬로 작업하면 데이터 경합이 발생할 수 있다.
특히 하나의 데이터 모델과 저장소에 조회와 변경 요청이 집중되면 복잡한 동시성 처리가 필요해질 수 있다.
복잡한 조회
사용자가 요구하는 형태의 데이터를 만들기 위해 여러 Table을 대상으로 복잡한 Query를 수행해야 할 수도 있다.
Table A ─┐
Table B ─┼→ JOIN / 복잡한 Query → 조회 결과
Table C ─┘
조회 요구사항이 복잡해질수록 Query의 복잡도가 증가하고 성능에도 영향을 줄 수 있다.
정리
전통적인 방식 자체가 문제가 되는 것은 아니다.
하지만 시스템이 복잡해지면서 읽기와 쓰기가 서로 다른 데이터 형태, 성능, 확장 방식과 비즈니스 로직을 요구하기 시작하면 하나의 모델이 너무 많은 책임을 담당하게 될 수 있다.
2 )Command와 Query의 분리
CQRS에서는 이러한 문제를 해결하기 위해 읽기와 쓰기에 서로 다른 모델을 사용한다.
Application
│
┌───────────┴───────────┐
↓ ↓
Command Model Query Model
↓ ↓
데이터 변경 데이터 조회
Command
Command는 시스템의 상태를 변경하기 위한 요청이다.
단순히 데이터를 중심으로 표현하기보다 어떤 작업을 수행할 것인지를 중심으로 구성한다.
데이터 중심
→ Book 데이터 변경
작업 중심
→ 도서 등록
→ 도서 가격 변경
→ 도서 삭제
강의에서 다루는 Kafka 기반 구조처럼 Command를 Queue에 전달하여 비동기적으로 처리하는 방식도 사용할 수 있다.
Command
↓
Queue
↓
Command 처리
↓
데이터 변경
이를 통해 요청을 전달하는 부분과 실제 작업을 처리하는 부분을 분리할 수 있다.
Query
Query는 데이터를 조회하기 위한 요청이다.
Query는 데이터베이스의 상태를 변경하지 않고 조회 결과를 반환한다.
Query
↓
Read Model
↓
DTO
↓
Client
조회에 필요한 형태에 맞는 DTO를 구성함으로써 쓰기 모델의 복잡한 비즈니스 로직과 조회 로직을 분리할 수 있다.
저장소까지 분리하는 구조
CQRS의 핵심은 Command와 Query의 책임과 모델을 분리하는 것이다.
필요한 경우 여기서 더 나아가 읽기와 쓰기의 저장소 자체를 분리할 수도 있다.
Command
↓
Write Model
↓
Write Database
Query
↓
Read Model
↓
Read Database
이렇게 구성하면 각각의 작업에 적합한 데이터베이스와 데이터 구조를 선택할 수 있다.
다음 실습에서는 이 구조를 이용하여 쓰기 저장소와 읽기 저장소를 서로 다른 데이터베이스로 구성한다.
3 ) CQRS의 장점
읽기와 쓰기의 독립적인 확장
읽기와 쓰기의 부하가 서로 다르다면 각 영역을 독립적으로 확장할 수 있다.
Read 요청 증가
↓
Read 영역 확장
Write 요청 증가
↓
Write 영역 확장
전체 시스템을 동일한 비율로 확장하지 않고 필요한 부분에 맞게 자원을 조정할 수 있다.
작업에 최적화된 데이터 구조
읽기 저장소와 쓰기 저장소를 분리하면 각 작업에 적합한 Schema를 구성할 수 있다.
Write Model
데이터 변경과 비즈니스 로직 처리에 적합한 구조를 사용할 수 있다.
Read Model
조회에 적합한 구조를 구성하여 복잡한 Join을 줄일 수 있다.
예를 들어 읽기에 최적화된 Materialized View와 같은 형태를 이용하면 미리 구성된 데이터를 조회하여 복잡한 Query를 줄일 수 있다.
관심사의 분리
복잡한 비즈니스 로직은 주로 쓰기 모델에서 처리하고 읽기 모델은 상대적으로 단순한 조회에 집중하도록 구성할 수 있다.
Command Model
→ Validation
→ Business Logic
→ 데이터 변경
Query Model
→ 데이터 조회
→ DTO 반환
각 모델의 책임이 명확해지므로 하나의 모델이 읽기와 쓰기의 모든 요구사항을 처리하는 문제를 줄일 수 있다.
보안
Command와 Query의 진입점과 책임이 분리되므로 데이터 변경 작업에 대해 별도의 권한이나 보안 정책을 적용하는 구조를 구성할 수 있다.
4 ) CQRS 적용 시 고려할 문제
CQRS는 읽기와 쓰기를 분리함으로써 여러 장점을 얻을 수 있지만 시스템 구조 자체는 기존 CRUD 방식보다 복잡해질 수 있다.
구조의 복잡성
단순했던 하나의 모델을 Command와 Query로 분리해야 하므로 관리해야 하는 구성 요소가 증가한다.
특히 Event Sourcing과 같은 패턴까지 함께 적용하면 전체 구조의 복잡성이 더욱 증가할 수 있다.
따라서 CQRS가 항상 기존 CRUD 방식보다 좋은 것은 아니다.
Messaging 처리
Command 처리 이후 Event를 발행하여 다른 시스템의 데이터를 갱신하는 구조에서는 Messaging을 사용하게 될 수 있다.
Command
↓
Write 처리
↓
Event 발행
↓
Message Broker
↓
Read Model 갱신
이 경우 애플리케이션에서는 정상적인 Message 처리뿐 아니라 Message 전송 실패나 중복 Message와 같은 상황도 고려해야 한다.
데이터 일관성
읽기와 쓰기의 저장소를 분리하고 Message를 통해 데이터를 전달하면 두 저장소가 항상 같은 순간에 갱신되는 것은 아니다.
Write Database 갱신
↓
Event 전달
↓
Read Database 갱신
따라서 Write Database의 변경 사항이 Read Database에 반영되기 전까지 일시적으로 두 데이터의 상태가 다를 수 있다.
CQRS 구조를 설계할 때에는 이러한 데이터 일관성 문제를 함께 고려해야 한다.
ORM 사용의 제약
강의에서는 CQRS 적용 시 읽기와 쓰기의 모델을 서로 다른 목적에 맞게 구성하므로, 단일 데이터베이스 Schema를 기준으로 ORM을 통해 전체 모델을 단순하게 자동 생성하는 방식과는 맞지 않을 수 있다는 점을 다루었다.
각 모델이 자신의 목적에 맞는 구조를 가지기 때문에 모델과 데이터 구조를 별도로 설계해야 할 수 있다.
5 ) CQRS를 적용할 수 있는 경우
CQRS는 시스템의 복잡성이 증가하는 대신 읽기와 쓰기를 독립적으로 구성할 수 있다는 장점을 얻는 패턴이다.
따라서 이러한 분리가 실제로 필요한 상황에서 사용하는 것이 적절하다.
다음과 같은 경우 적용을 고려할 수 있다.
- 많은 사용자가 동일한 데이터에 병렬로 접근하는 경우
- 복잡한 프로세스나 Domain Model을 기반으로 작업을 처리하는 경우
- 읽기 성능과 쓰기 성능을 서로 독립적으로 조정해야 하는 경우
- 읽기와 쓰기를 명확하게 분리하여 구현하려는 경우
- 하나의 시스템이 장기간 운영되면서 여러 Version을 가지며 지속적으로 변경되는 경우
- 다른 시스템과의 통합이 필요한 경우
반대로 다음과 같은 경우에는 CQRS로 인한 구조적 복잡성이 더 큰 부담이 될 수 있다.
- Domain이나 Business Logic이 단순한 경우
- 단순한 CRUD 중심의 애플리케이션
단순한 CRUD
→ 기존 구조가 적합할 수 있음
복잡한 Domain
+ 읽기/쓰기 요구사항 차이
+ 독립적인 확장 필요
→ CQRS 적용 고려
정리
CQRS (Command Query Responsibility Segregation)는 데이터 변경을 담당하는 Command와 데이터 조회를 담당하는 Query의 책임을 분리하는 패턴이다.
전통적인 구조에서는 읽기와 쓰기가 하나의 데이터 모델을 공유하므로, 시스템이 복잡해질수록 하나의 모델이 조회와 변경에 필요한 서로 다른 요구사항을 모두 담당하게 될 수 있다.
읽기와 쓰기는 요구되는 데이터 형태와 부하가 서로 다를 수 있으며, 동일한 데이터에 병렬로 접근할 경우 데이터 경합이 발생할 수도 있다.
CQRS에서는 Command Model과 Query Model을 분리하여 각각의 책임에 집중하도록 구성한다.
Command는 데이터 자체보다 수행할 작업을 중심으로 구성하며, Message Queue 등을 이용하여 비동기적으로 처리할 수 있다.
Query는 데이터를 변경하지 않고 조회 결과를 DTO 등의 형태로 반환한다.
Write Database와 Read Database를 분리하면 각각의 작업에 적합한 데이터 스키마와 저장 기술을 사용할 수 있다.
읽기 저장소에서는 Materialized View 등을 활용하여 복잡한 조회를 단순화할 수 있으며, 읽기와 쓰기를 독립적으로 확장하고 최적화할 수 있다.
CQRS는 성능과 확장성, 보안, 관심사 분리 측면에서 장점이 있지만 구조 자체의 복잡성이 증가한다.
Messaging을 함께 사용하는 경우 Message 처리 실패, 중복 Message, 데이터 일관성 등의 문제를 고려해야 한다.
CQRS는 복잡한 Domain이나 읽기와 쓰기의 요구사항이 크게 다른 시스템에서 고려할 수 있으며, 단순 CRUD 애플리케이션에서는 불필요한 복잡성을 만들 수 있다.
댓글