TL;DR: 상품 좋아요 기능 설계 중 실시간 COUNT vs 집계 테이블 사이에서 고민했고, 성능과 정합성의 트레이드오프를 정리하며 집계 테이블을 선택했다.
시작: 좋아요 수? 그냥 COUNT면 되지 않을까?
이커머스 시스템 설계를 하면서 상품 좋아요 기능이 필요했다. 요구사항을 보니 상품 목록에서 각 상품의 좋아요 수를 보여줘야 하고, 심지어 “좋아요 순 정렬”도 지원해야 했다.
처음엔 단순하게 생각했다. ProductLike 테이블에서 실시간으로 COUNT 쿼리 날리면 되지 않을까?
-- 이렇게 하면 되지 않을까?
SELECT COUNT(*) FROM product_likes
WHERE product_id = ? AND deleted = false;
설계하면서 드는 의구심들
1. 상품 목록 조회가 걱정됐다
상품 목록 20개를 조회할 때 각각의 좋아요 수를 가져오려면… 21번의 쿼리가 필요하다.
상품 목록 조회: 1번
상품별 좋아요 수: 20번
총 21번의 DB 호출
JOIN으로 한 번에 가져올 수 있을까 고민해 봤지만, ProductLike를 JOIN 하면 상품 하나당 여러 행이 나와서 복잡해진다.
2. 좋아요 순 정렬이 더 큰 문제였다
“좋아요 순으로 상품을 정렬해서 보여주세요”라는 요구사항을 보는 순간 머리가 아팠다.
-- 이런 쿼리를 짜야 하나?
SELECT p.*, COUNT(pl.id) as like_count
FROM products p
LEFT JOIN product_likes pl ON p.id = pl.product_id AND pl.deleted = false
GROUP BY p.id
ORDER BY like_count DESC;
동작은 하겠지만, 데이터가 많아지면 GROUP BY와 COUNT 연산이 부담될 것 같았다.
3. 캐시를 고려해봤지만…
Redis 같은 캐시를 쓰면 어떨까 생각해 봤다. 하지만 좋아요가 등록/취소될 때마다 캐시를 갱신해야 하고, 캐시와 DB 간 불일치 가능성도 있었다. 게다가 지금 단계에서 Redis를 도입하는 게 과한 것 같기도 하고.
집계 테이블이라는 선택지
그때 떠오른 게 집계 테이블이었다. 좋아요 수만 별도로 관리하는 테이블을 만드는 거다.
CREATE TABLE product_statistic (
id BIGINT PRIMARY KEY,
product_id BIGINT UNIQUE,
like_count INT DEFAULT 0,
updated_at DATETIME
);
장점들이 명확했다
- 조회 성능: 상품 목록 조회 시 단순 JOIN으로 해결
- 정렬 성능: 인덱스 걸린 숫자 컬럼으로 바로 정렬
- 확장성: 나중에 “주간 좋아요”, “월간 좋아요” 같은 기능도 추가 가능
하지만 고민도 많았다
- 데이터 중복: 같은 정보를 두 곳에 저장
- 정합성 이슈: 좋아요 등록/취소 시 두 테이블을 모두 업데이트해야 함
- 복잡성 증가: 단순했던 로직이 복잡해짐
내가 선택한 방식과 그 이유
결국 집계 테이블을 도입하기로 했다. 결정적인 이유는 사용자 경험이었다.
상품 목록은 사용자가 가장 자주 접근하는 페이지다. 여기서 성능이 떨어지면 전체 서비스 경험이 나빠진다. 반면 좋아요 등록/취소는 상대적으로 빈도가 낮은 작업이다.
- 읽기(조회): 매우 빈번, 성능 중요
- 쓰기(좋아요): 상대적으로 드물음
“읽기 최적화 vs 쓰기 복잡성” 트레이드오프에서 읽기를 선택한 것이다.
설계된 구조
-- 상품 목록 + 좋아요 수 조회 (성능 최적화)
SELECT p.*, ps.like_count
FROM products p
LEFT JOIN product_statistic ps ON p.id = ps.product_id
ORDER BY ps.like_count DESC;
-- 좋아요 등록 시 (트랜잭션 내에서)
INSERT INTO product_statistic (...);
UPDATE product_statistic SET like_count = like_count + 1 WHERE product_id = ?;
정합성 보장 방안도 고려했다
- 트랜잭션: 좋아요 등록/취소와 카운트 업데이트를 한 트랜잭션에서 처리
- 배치 보정: 주기적으로 실제 COUNT와 집계값 비교하여 보정
- 락 전략: 동시성 이슈 방지를 위한 적절한 락 사용
아직 남은 고민들
설계는 했지만 여전히 고민되는 부분들이 있다.
언제까지 집계 테이블을 써야 할까? 지금은 좋아요 수만 집계하지만, 나중에 조회수, 평점, 리뷰 수 등도 추가되면 집계 테이블이 많아질 수 있다. 어느 시점에서 다른 방식(예: 검색 엔진, 캐시)을 고려해야 할까?
정합성 이슈가 실제로 발생하면? 이론적으로는 트랜잭션으로 해결되지만, 실제 운영에서 예상치 못한 상황이 생길 수 있다. 모니터링과 복구 전략을 어떻게 세워야 할까?
배운 점
설계 단계에서 성능과 정합성 사이의 트레이드오프를 고민해 보는 경험이 값졌다.
완벽한 해답은 없다는 걸 깨달았다. 중요한 건 현재 상황에서 가장 합리적인 선택을 하고, 그 선택의 이유를 명확히 하는 것이다.
집계 테이블 도입은 현재 요구사항과 예상 트래픽을 고려했을 때 가장 현실적인 선택이었다. 나중에 더 좋은 방법을 찾게 되면 그때 다시 리팩토링하면 된다.
설계는 정답을 찾는 게 아니라, 합리적인 근거를 가진 선택을 하는 과정이라는 걸 다시 한번 느꼈다.
'프로젝트' 카테고리의 다른 글
| 하나로 묶인 트랜잭션을 쪼개다: 이벤트 기반 주문 시스템 리팩토링기 (2) | 2025.09.25 |
|---|---|
| Cache-Aside 적용기: DB 부하 감소와 응답 속도 안정화 (2) | 2025.09.25 |
| 동시성 제어 전략 선택기: 상황에 맞는 락을 고르는 기준 (0) | 2025.09.24 |
| Value Class 도입기: String에서 타입 안전성까지 (0) | 2025.09.22 |