
CQRS와 조회 전용 저장소, 그리고 카테고리-상품 관계 고민기
도메인 주도 설계를 배우면서 겪은 고민
최근 도메인 주도 개발(DDD)에 관한 책을 읽으면서 여러 가지 설계에 대한 깊은 고민을 하게 되었다.
특히 관심을 끈 부분은 CQRS(Command Query Responsibility Segregation) 개념이었다. 이 글에서는 공부한 CQRS 개념과 함께, 카테고리와 상품 사이의 관계를 어떻게 효율적으로 설계할 수 있는지 정리해 보려고 한다.
쓰기 모델과 조회 모델을 분리한 이유
일반적인 웹 애플리케이션은 데이터베이스 하나에서 읽기 쓰기를 모두 처리한다. 이렇게 구현해도 초반에는 큰 문제없이 잘 동작하지만, 서비스가 성장하면서 읽기(조회) 트래픽이 급증하면 다음과 같은 문제가 발생할 수 있다.
읽기 요청 증가로 인한 데이터베이스 성능 저하
복잡한 검색 및 필터링으로 인한 성능 한계
이를 해결하기 위한 방법 중 하나가 CQRS 이다.
CQRS의 핵심 개념
쓰기(명령)와 읽기(쿼리)의 책임을 분리
쓰기 작업은 RDB에 반영하고, 읽기는 ElasticSearch나 Redis 같은 빠른 저장소를 활용
CQRS 흐름
쓰기 발생: 데이터를 RDB에 저장
이벤트 또는 동기화: Kafka 등을 통해 조회 전용 저장소(ElasticSearch)에 데이터 동기화
조회 요청 처리: 조회 전용 저장소에서 빠르게 검색/조회
이를 통해 조회 성능 향상과 시스템 부하 분산을 얻을 수 있다. 하지만 이벤트 동기화 과정에서 구현 복잡도가 늘어나고, 실시간성이 필요한 경우 동기화 지연이 단점으로 작용할 수 있다.
간단 예시: CQRS 적용 시나리오상품 생성 시:
상품 정보를 RDB에 저장
이벤트를 발생 (예: "상품 생성됨")
이벤트 소비자가 ElasticSearch에 해당 상품 정보를 인덱싱
사용자 조회 시:
ElasticSearch에서 상품 목록 검색 -> 빠른 검색 결과 반환
카테고리와 상품 관계 설계
카테고리와 상품 간의 설계는 애플리케이션 성능, 특히 페이징 처리와 깊은 관련이 있다.
데이터 조회와 성능에 큰 영향을 미치는 두 가지 대표적인 설계 방식을 자세히 살펴보자.
N-1 관계 (상품 -> 카테고리 참조)
N-1 관계는 상품(Product)이 특정 카테고리(Category)를 직접 참조하는 방식이다. 일반적으로 상품이 반드시 하나의 카테고리에만 속할 때 이 방식을 사용한다.
@Entity
public class Product {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "category_id")
private Category category;
}
장점
특정 카테고리에 속한 상품을 효율적으로 페이징 가능하다.
category_id
컬럼에 DB 인덱스를 설정하면 빠른 조회가 가능하다.
단점
카테고리에서 직접적으로 상품 목록에 접근하는 것이 어렵다.
N+1 문제가 발생할 가능성이 높다.
이 문제는 JPA의 Fetch Join이나 EntityGraph 같은 기법으로 해결할 수는 있으나 본 글의 내용과는 관련성이 떨어지기 때문에 자세히 다루진 않도록 하겠다.
M-N 관계 (상품 <-> 카테고리 ID로 연결)
M-N 방식은 하나의 상품이 여러 개의 카테고리에 속할 때 사용하는 구조로, 중간 테이블을 사용하여 다대다 관계를 처리한다. 특히 ID기반으로 연관관계를 표현하여 직접적인 엔티티 참조를 피하는 방식이다.
@Entity
public class Product {
@Id
private Long id;
@ElementCollection
@CollectionTable(name = "product_category", joinColumns = @JoinColumn(name = "product_id"))
@Column(name = "category_id")
private Set<Long> categoryIds;
}
장점
엔티티 간 직접적인 양방향 참조 없이 ID로만 관계를 맺어 결합도를 낮춘다.
데이터가 많은 때도 페이징 및 필터링을 DB에서 효과적으로 처리할 수 있다.
단점
특정 카테고리에 속한 상품을 조회하려면 추가적인 쿼리가 필요하다.
카테고리와 상품 정보를 함께 조회할 경우 성능 저하가 발생할 수 있다.
어떤 방식을 선택해야 할까?
관계를 설계할 때는 애플리케이션의 사용 패턴과 비즈니스 요구사항을 깊게 고민해야 한다. 각각의 관계 방식은 다음과 같은 상황에 효과적이다.
N-1 방식
상품이 주로 하나의 카테고리에 속하는 경우
상품 중심으로 자주 조회하는 기능이 필요할 때
조회 성능 및 빠른 페이징이 중요한 경우
M-N 방식
하나의 상품이 여러 카테고리에 동시에 속할 수 있는 경우
다양한 조건으로 필터링하거나 복잡한 조회가 필요한 경우
많은 데이터를 효율적으로 처리해야 할 경우
글을 마무리하며
이 글을 작성하면서 설계 방식 하나를 선택하는 일이 얼마나 중요한 고민인지 새삼 느끼게 되었다.
이론적으로는 두 방식 모두 장단점이 있기 때문에 어느 하나가 더 우월하다 말할 수는 없겠지만, 결국 실제 서비스에서 어떤 방식이 더 효율적일지는 요구사항을 고려하여 더 효율적인 선택을 하는건 개발자의 몫인 것 같다.
앞으로도 단순히 이론에 그치지 않고 실무에서 끊임없이 고민하며 상황에 맞는 좋은 설계 방식을 찾아가야겠다는 생각이 들었으며 끝으로 이 글의 내용과 비슷한 고민을 하고 있는 사람들에게 조금이나마 도움이 되었기를 바란다.
댓글을 작성해보세요.