본문 바로가기
Server

CQRS

by Ahngyuho 2025. 2. 19.

CQRS (Command Query Responsibility Segregation)

1. 개요

CQRS는 **Command (쓰기)**와 Query (읽기) 작업을 분리하는 디자인 패턴입니다.

  • Command (쓰기): CREATE, UPDATE, DELETE
  • Query (읽기): SELECT

이를 통해 데이터 처리 효율성을 높이고, 부하를 분산할 수 있습니다.


2. CQRS vs DataSourceRouting

  • DataSource Routing
    • 주로 Master-Slave 데이터베이스 구조에서 사용됨
    • 트랜잭션이 필요한 경우, Master에서 처리
    • SELECT 요청은 Slave에서 처리하여 부하를 분산
  • CQRS
    • 쓰기와 읽기 로직을 코드 레벨에서 분리
    • Master-Slave 구조가 아니라, 별도의 Read Storage를 운영
    • 데이터 동기화를 **이벤트 기반(Kafka 등)**으로 처리

3. CQRS 구성 방식

1) 코드 단에서 분리

  • 단일 프로젝트 내에서
    • BoardCommandService (쓰기 전용)
    • BoardQueryService (읽기 전용)
  • 각 서비스는 별도의 의존성 주입을 받음
  • 트랜잭션 관리도 개별적으로 수행

💡 트랜잭션을 반드시 분리할 필요는 없음

  • 같은 DB를 사용한다면, 하나의 트랜잭션으로 처리 가능
  • 그러나 읽기 요청이 많다면 분리하는 것이 효율적

2) 서버 분리

  • 각각의 서비스(쓰기/읽기)를 별도의 서버로 운영
    • BoardCommandService → 쓰기 서버
    • BoardQueryService → 읽기 서버
  • 서버를 분리하면 수평 확장이 용이

4. CQRS의 장점

부하 분산

  • 보통 SELECT 요청이 압도적으로 많음 → 읽기 전용 서버 확장 가능

확장성

  • BoardQueryService를 여러 개 배포하여 성능 최적화 가능

데이터 동기화 방식

  • Master-Slave DB 방식이 아님
  • 이벤트 기반 아키텍처 활용 (Kafka 등)

5. Kafka를 활용한 CQRS

1️⃣ 이벤트 기반 데이터 동기화

  1. Command 이벤트 발생 → 데이터 저장
  2. Kafka 특정 토픽에 이벤트 전송
  3. 구독하는 Read Storage에 데이터 반영

2️⃣ 구조

scss
복사편집
BoardCommandService → Kafka (이벤트 브로커) → BoardQueryService (Read Storage)

이벤트 기반 CQRS 아키텍처 구축
읽기 저장소(Read Storage)를 별도로 운영하여 성능 최적화


6.  이벤트 기반 CQRS 구현 코드

 

 

board-command-service yaml

 

server:
  port: 8082

spring:
  application:
    name: board-query-service
  kafka:
    bootstrap-servers: 10.10.10.103:9092 # (카프카(브로커 서버) 주소):(포트)
    consumer:
      # 카프카로 데이터를 주고 받을 때, 객체 형태로 주고 받을 수 있게 특정 형태로 해석을 하기 위함(역직렬화)
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer  # ✅ JSON 역직렬화
      group-id: agh-group
      #신뢰 가능한 객체가 있는 패키지를 설정!
      properties:
        spring.json.trusted.packages: "*"

    producer:
      # 카프카로 데이터를 주고 받을 때, 객체 형태로 주고 받을 수 있게 특정 형태로 전달하기 위함(직렬화)
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer  # ✅ JSON 직렬화

 

 

 

board-query-service yaml

spring:
  application:
    name: board-query-service
  kafka:
    bootstrap-servers: 10.10.10.103:9092
    consumer:
      # 카프카로 데이터를 주고 받을 때, 객체 형태로 주고 받을 수 있게 특정 형태로 해석을 하기 위함(역직렬화)
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer  # ✅ JSON 역직렬화
      group-id: agh-group
      #신뢰 가능한 객체가 있는 패키지를 설정!
      properties:
        spring.json.trusted.packages: "*"

    producer:
      # 카프카로 데이터를 주고 받을 때, 객체 형태로 주고 받을 수 있게 특정 형태로 전달하기 위함(직렬화)
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer  # ✅ JSON 직렬화

 

board-command-service 

BoardService

@Service
@RequiredArgsConstructor
public class BoardService {
    private final BoardRepository boardRepository;
    private final KafkaTemplate<String, BoardCreatedEvent> kafkaTemplate;

    public void save(Board board) {
        boardRepository.save(board);

        //저장 이벤트 발행!
        kafkaTemplate.send("board-created", BoardCreatedEvent.of(board));
    }
}

 

DTO

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
//프로듀서되는 데이터와 컨슈머되는 데이터가 다를 수 있어서 따로 객체를 만듦
public class BoardCreatedEvent {
    private Long idx;
    private String title;
    private String contents;

    public static BoardCreatedEvent of(Board board) {
        return BoardCreatedEvent.builder()
                .idx(board.getIdx())
                .title(board.getTitle())
                .contents(board.getContents())
                .build();
    }

    public Board toEntity() {
        return Board.builder()
                .idx(idx)
                .title(title)
                .contents(contents)
                .build();
    }
}

 

board-query-service 

 

BoardService

@Service
@RequiredArgsConstructor
public class BoardService {
    private final BoardRepository boardRepository;

    @KafkaListener(topics = "board-created", groupId = "agh-group", properties = {
            "spring.json.value.default.type:com.example.boardqueryservice.board.event.BoardCreatedEvent",
            "spring.json.use.type.headers:false"
    })
    public void getBoardCreatedEvent(BoardCreatedEvent board) {
        boardRepository.save(board.toEntity());
    }

    public BoardDto.Response get(Long idx) {

        Board board = boardRepository.findByIdx(idx).orElseThrow();
        return BoardDto.Response.fromEntity(board);
    }

    public List<BoardDto.Response> list() {
        return boardRepository.findAll().stream().map(BoardDto.Response::fromEntity).collect(Collectors.toList());
    }
}

 

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
public class BoardCreatedEvent {
    private Long idx;
    private String title;
    private String contents;

    public Board toEntity() {
        return Board.builder()
                .idx(idx)
                .title(title)
                .contents(contents)
                .build();
    }

}

 

 

 위 코드의 구조를 그림으로 표현하면 다음과 같습니다.

 

 

 

 

 

7. 정리

CQRS는 단순한 Master-Slave DB 구조가 아니라, 쓰기와 읽기 로직을 완전히 분리하는 방식입니다.
Kafka와 같은 이벤트 브로커를 활용하면 데이터 동기화가 원활하게 이루어지고 확장성도 뛰어납니다.

 

 

 

 

'Server' 카테고리의 다른 글

동시성 문제  (0) 2025.02.26
PinPoint  (0) 2025.02.21
웹 소켓(Web Socke!)  (0) 2025.02.17
멀티파트 폼 데이터(Multipart Form Data)  (0) 2025.01.11
Spring boot 애플리케이션 ec2에 jar로 배포  (0) 2023.07.23