CQRS와 DDD

MONA·2025년 6월 29일

나혼공

목록 보기
79/92

CQRS (Command Query Responsibility Segregation)

명령과 조회 책임 분리

전통적인 CRUD 기반 애플리케이션에서는 같은 모델로 데이터를 읽기(Query)도 하고, 쓰기(Command)도 수행한다.
CQRS는 이 두가지를 명확히 분리하는 아키텍처 패턴.

when?

복잡한 도메인에서 유용함

  • 읽기와 쓰기 성격이 완전히 다른 경우
  • 읽기 성능을 최적화 해야 하는 경우
  • 복잡한 비즈니스 규칙이 쓰기 쪽에 집중이 되어 있는 경우
  • 확장성과 마이크로서비스 분리에 용이하게 하려는 경우

핵심 개념

Command : 데이터를 변경하는 요청(CUD)
Query : 데이터를 조회하는 요청(R)

예시

Java/Spring 기반

  1. Layer
src
├── command
│   ├── controller
│   ├── service
│   └── domain
├── query
│   ├── controller
│   ├── service
│   └── dto
// Command: 사용자 생성
@RestController
@RequestMapping("/users")
public class UserCommandController {
    @PostMapping
    public ResponseEntity<Void> createUser(@RequestBody CreateUserRequest request) {
        userCommandService.createUser(request);
        return ResponseEntity.ok().build();
    }
}

// Query: 사용자 조회
@RestController
@RequestMapping("/users")
public class UserQueryController {
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userQueryService.getUserById(id));
    }
}

단점 및 주의사항

  • 복잡도가 증가함(코드, DB 모델 이중화)
  • 초반에는 과도한 설계일 수 있다(소규모 프로젝트에는 과함)
  • 데이터 동기화 문제 발생 가능(Command -> Query 모델 전파 지연)

그래서 언제 쓰면 좋을까.

  • 쓰기보다 읽기가 훨씬 많은 시스템
  • 복잡한 도메인 로직을 명확히 분리하고 싶을 때
  • MSA로 확장할 예정이라면 고려해 볼 만함

CQRS와 DDD

DDD: 복잡한 도메인을 도메인 중심으로 설계하는 방법론
CQRS: 명령과 조회를 역할에 따라 분리하는 아키텍처 패턴

설계

src
├── api
│   ├── UserCommandController.java
│   └── UserQueryController.java
├── application
│   ├── command
│   │   ├── CreateUserCommand.java
│   │   └── CreateUserHandler.java
│   └── query
│       └── GetUsersHandler.java
├── domain
│   └── user
│       ├── User.java
│       ├── UserId.java
│       ├── UserNickname.java
│       ├── UserRepository.java
├── infrastructure
│   └── jpa
│       └── UserJpaRepository.java
│       └── UserReadModelRepository.java
└── querymodel
    └── UserReadModel.java

Command(쓰기)

  • Aggregate Root: 도메인의 핵심 엔티티 (비즈니스 규칙 포함)
  • Entity / Value Object: DDD의 핵심 구성 요소
  • Domain Event: 상태 변화 후 발생하는 이벤트
  • Command / CommandHandler: 상태 변경 요청과 그 처리기
  • Application Service: 유스케이스 구현
  • Repository: Aggregate 저장소 (JPA 등)

Query(읽기)

  • Read Model: 조회 전용 DTO/엔티티
  • Projection / Event Listener: Command에서 발생한 이벤트를 기반으로 ReadModel을 갱신
  • Query Handler: 특정 조회 목적에 맞춘 빠른 응답을 위한 처리기 (QueryDSL, Native SQL 등)

플로우

  1. 사용자 생성(Command)
    • UserCommandController → CreateUserCommand
    • CreateUserHandler가 User Aggregate 생성
    • 비즈니스 규칙 검증 및 UserCreatedEvent 발행
    • UserRepository를 통해 저장
  2. 사용자 조회 (Query)
    • UserQueryController → GetUserQuery
    • UserQueryHandler가 별도의 조회 전용 DB or DTO로 조회
      • ex: UserReadModel 또는 JdbcTemplate로 빠르게 읽기

핵심 이점

장점내용
책임 분리Command는 비즈니스 규칙 중심, Query는 성능 중심으로 설계
확장성쓰기/읽기 사이드를 독립적으로 확장하거나 배포 가능
성능 최적화읽기 전용 DB나 캐시 사용으로 조회 성능 향상
도메인 모델 명확화Command 영역에서는 풍부한 DDD 모델을 유지 가능

주의

  • 설계 복잡도 증가 -> 소규모 프로젝트에는 과함
  • Read 모델-Write 모델 간 동기화 필요(Eventual Consistancy)
  • 테스트, 디버깅의 어려움

결론

CQRS는 역할 분리를 명확히 하여 DDD를 더욱 구조적으로 구현할 수 있게 해준다.
DDD는 CQRS의 Command 영역에 깊이 있는 도메인 로직을 표현하는데 적합하다.

profile
고민고민고민

0개의 댓글