🤔 CQRS란 무엇인가
1. 전통적인 아키텍처의 한계
전통적인 모놀리식 아키텍처에서는 데이터베이스에 대한 읽기와 쓰기 작업이 동일한 모델과 저장소를 통해 처리된다. 이는 시스템이 단순할 때는 문제가 없지만, 복잡성이 증가하면 다음과 같은 문제점이 발생한다.
- 복잡한 도메인 모델: 읽기와 쓰기를 모두 지원하기 위해 도메인 모델이 복잡해진다.
- 성능 저하: 읽기와 쓰기 작업이 동일한 데이터베이스에 집중되어 성능 병목이 발생한다.
- 확장성 한계: 읽기와 쓰기 작업을 독립적으로 확장하기 어렵다.
2. CQRS의 개념과 예시 사례
CQRS는 명령과 조회의 책임을 분리하여 이러한 문제를 해결한다. 즉, 데이터를 변경하는 작업과 데이터를 조회하는 작업을 별도의 모델과 저장소를 사용하여 처리한다.
- Command 모델: 데이터 변경 작업을 처리하며, 비즈니스 로직과 데이터 무결성에 집중한다.
- Query 모델: 데이터 조회 작업을 처리하며, 성능과 확장성에 최적화되어 있다.
예시 사례:
- 이벤트 티켓팅 시스템: 콘서트 티켓 예매 시스템에서는 티켓의 판매와 조회가 빈번하게 일어난다. 티켓 판매는 재고 감소와 같은 복잡한 비즈니스 로직이 수반되며, 데이터 무결성이 중요하다. 반면에 티켓 정보 조회는 대량의 트래픽을 처리해야 하므로 읽기 성능이 중요하다. 이때 CQRS를 적용하여 쓰기와 읽기 모델을 분리하면 효율적인 시스템 운영이 가능하다.
🤨 이벤트 소싱이란 무엇인가
1. 상태 저장 방식과의 비교
전통적인 애플리케이션에서는 현재 상태만을 데이터베이스에 저장한다. 그러나 이벤트 소싱에서는 상태 변경 이벤트를 순서대로 저장하고, 이 이벤트들을 재생하여 현재 상태를 재구성한다.
- 상태 저장 방식: 현재 상태만을 저장하며, 과거 상태나 변경 내역을 알기 어렵다.
- 이벤트 소싱 방식: 모든 상태 변경 이벤트를 저장하여, 과거의 모든 상태를 재구성할 수 있다.
2. 이벤트 소싱의 예시 사례
- 금융 거래 시스템: 은행의 거래 내역 관리 시스템에서는 모든 거래 이벤트를 기록해야 한다. 이벤트 소싱을 적용하면 각 거래 이벤트를 저장하여 계좌의 현재 잔액뿐만 아니라 과거의 거래 내역도 추적할 수 있다.
- 쇼핑몰 주문 처리 시스템: 주문 생성, 취소, 배송 등의 모든 상태 변경을 이벤트로 저장하여 주문의 상태 변화를 추적할 수 있다.
🙏 CQRS와 이벤트 소싱의 결합
1. 왜 함께 사용하는가
CQRS와 이벤트 소싱은 서로 보완적인 관계에 있다. CQRS의 쓰기 모델에서 이벤트 소싱을 적용하면 다음과 같은 이점을 얻을 수 있다.
- 복잡한 비즈니스 로직 관리: 이벤트를 통해 복잡한 상태 변화를 효과적으로 관리한다.
- 데이터 일관성 유지: 이벤트 스트림을 통해 쓰기와 읽기 모델의 데이터 일관성을 유지한다.
- 확장성 향상: 이벤트를 활용하여 시스템 간의 통합이 용이하며, 확장성이 높아진다.
2. 아키텍처 개요
├── api
│ └── OrderCommandController.java
│ └── OrderQueryController.java
├── application
│ └── OrderService.java
├── domain
│ ├── OrderAggregate.java
│ ├── command
│ │ └── CreateOrderCommand.java
│ ├── event
│ │ └── OrderCreatedEvent.java
├── infrastructure
│ ├── repository
│ │ └── OrderRepository.java
│ ├── entity
│ │ └── OrderEntity.java
│ └── handler
│ └── OrderEventHandler.java
- api: 클라이언트의 요청을 처리하는 컨트롤러들이 위치한다.
- application: 비즈니스 로직을 처리하는 서비스가 위치한다.
- domain: 도메인 모델과 명령, 이벤트 클래스가 위치한다.
- infrastructure: 데이터베이스 접근, 이벤트 핸들러 등 인프라 관련 코드가 위치한다.
✅ Axon Framework 소개
Axon Framework는 자바 기반의 오픈 소스 프레임워크로, DDD(Domain-Driven Design), CQRS, 이벤트 소싱 패턴을 손쉽게 구현할 수 있도록 지원한다. 주요 특징은 다음과 같다.
- 명령, 이벤트, 쿼리 메시징 지원: 명령과 이벤트를 간편하게 정의하고 처리할 수 있다.
- 이벤트 저장소 제공: 이벤트 소싱을 위한 이벤트 저장소를 내장하고 있다.
- 확장성: 마이크로서비스 아키텍처와의 통합이 용이하다.
- Spring Boot와의 통합: Spring 환경에서 쉽게 설정하고 사용할 수 있다.
💻 자바 스프링으로 구현하기
1. 프로젝트 설정
의존성 추가 (build.gradle)
dependencies {
// Axon Framework
implementation 'org.axonframework:axon-spring-boot-starter:4.7.3'
}
2. 도메인 모델 정의
OrderAggregate
@Aggregate
public class OrderAggregate {
@AggregateIdentifier
private String orderId;
private String product;
private int quantity;
private String status;
public OrderAggregate() {}
// 명령 처리자: 주문 생성 명령 처리
@CommandHandler
public OrderAggregate(CreateOrderCommand command) {
// 주문 생성 이벤트 발행
apply(new OrderCreatedEvent(
command.getOrderId(),
command.getProduct(),
command.getQuantity()
));
}
@EventSourcingHandler
protected void on(OrderCreatedEvent event) {
this.orderId = event.getOrderId();
this.product = event.getProduct();
this.quantity = event.getQuantity();
this.status = "CREATED";
}
}
- OrderAggregate는 주문 도메인의 Aggregate Root이다.
- @Aggregate: Axon Framework에서 Aggregate임을 나타낸다.
- @AggregateIdentifier: Aggregate의 식별자 필드이다.
- @CommandHandler: 명령을 처리하는 메서드이다.
- @EventSourcingHandler: 이벤트를 기반으로 상태를 변경한다.
3. 명령(Command) 처리
CreateOrderCommand
@Getter
@RequiredArgsConstructor
public class CreateOrderCommand {
@TargetAggregateIdentifier
private final String orderId;
private final String product;
private final int quantity;
}
- CreateOrderCommand는 주문 생성 명령을 나타낸다.
- @TargetAggregateIdentifier: 명령이 적용될 Aggregate를 식별한다.
OrderCommandController
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderCommandController {
private final CommandGateway commandGateway;
@PostMapping
public String createOrder(@RequestBody CreateOrderRequest request) {
String orderId = UUID.randomUUID().toString();
CreateOrderCommand command = new CreateOrderCommand(
orderId,
request.getProduct(),
request.getQuantity()
);
commandGateway.sendAndWait(command);
return orderId;
}
}
CreateOrderRequest
public record CreateOrderRequest (String product, int quantity){
}
- OrderCommandController는 클라이언트의 주문 생성 요청을 처리한다.
- CommandGateway를 사용하여 명령을 전송한다.
4. 이벤트(Event) 발행 및 저장
OrderCreatedEvent
@Getter
@AllArgsConstructor
public class OrderCreatedEvent {
private final String orderId;
private final String product;
private final int quantity;
}
- OrderCreatedEvent는 주문이 생성되었을 때 발생하는 이벤트이다.
- 이벤트는 불변 객체로 설계한다.
5. 조회(Query) 모델 업데이트
OrderEntity
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class OrderEntity {
@Id
private String orderId;
private String product;
private int quantity;
private String status;
}
- OrderEntity는 조회 모델을 위한 엔티티이다.
- JPA를 사용하여 데이터베이스에 저장된다.
OrderEventHandler
@Component
@RequiredArgsConstructor
public class OrderEventHandler {
private final OrderRepository orderRepository;
@EventHandler
public void on(OrderCreatedEvent event) {
OrderEntity order = new OrderEntity(
event.getOrderId(),
event.getProduct(),
event.getQuantity(),
"CREATED"
);
orderRepository.save(order);
}
}
- OrderEventHandler는 이벤트를 수신하여 조회 모델을 업데이트한다.
- @EventHandler: 이벤트를 처리하는 메서드에 적용한다.
OrderRepository
public interface OrderRepository extends JpaRepository<OrderEntity, String> {
}
6. 조회 API 구현
OrderQueryController
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderQueryController {
private final OrderRepository orderRepository;
@GetMapping
public List<OrderEntity> getAllOrders() {
return orderRepository.findAll();
}
@GetMapping("/{orderId}")
public OrderEntity getOrder(@PathVariable String orderId) {
return orderRepository.findById(orderId).orElse(null);
}
}
- OrderQueryController는 주문 조회 요청을 처리한다.
- 조회 모델을 사용하여 데이터를 반환한다.
😯 실제 적용 시 고려사항
1. 데이터 일관성 관리
CQRS와 이벤트 소싱을 사용하면 최종 일관성(Eventual Consistency)을 가지게 된다. 쓰기 작업 후 읽기 모델이 업데이트되기까지 시간이 걸릴 수 있으므로, 비즈니스 요구사항에 따라 적절한 일관성 관리 전략이 필요하다.
- 즉각적인 일관성 필요 시: 이벤트 핸들러를 동기적으로 처리하거나, 읽기와 쓰기 모델이 동일한 데이터베이스를 사용하도록 설계한다.
- 최종 일관성 허용 시: 비동기 이벤트 처리를 통해 시스템 성능을 향상시킨다.
2. 성능 최적화
- 스냅샷(Snapshot): 이벤트 수가 많아지면 스냅샷을 사용하여 Aggregate 재생 속도를 향상시킬 수 있다.
- 비동기 처리: 이벤트 핸들러를 비동기로 처리하여 쓰기 작업의 응답 시간을 줄일 수 있다.
- 데이터베이스 분리: 읽기와 쓰기 모델이 별도의 데이터베이스를 사용하여 각 작업에 최적화될 수 있다.
3. 이벤트 버스 선택
- Axon Server: AxonIQ에서 제공하는 전용 서버로, 이벤트 저장 및 전파를 효율적으로 처리한다.
- Kafka, RabbitMQ: 외부 메시지 브로커를 사용하여 이벤트를 전파할 수 있다.
4. 기업 적용 사례
- 넷플릭스: 마이크로서비스 간의 통신에 Kafka를 사용하여 이벤트 기반 아키텍처를 구현하였다.
- 우버: 대규모 데이터 처리를 위해 이벤트 소싱과 CQRS 패턴을 적용하여 시스템의 확장성을 높였다.
'DDD' 카테고리의 다른 글
[DDD] DDD란 무엇이고 도대체 왜 쓸까? (3) | 2024.09.15 |
---|