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️⃣ 이벤트 기반 데이터 동기화
- Command 이벤트 발생 → 데이터 저장
- Kafka 특정 토픽에 이벤트 전송
- 구독하는 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 |