프로젝트

좋아요 수 집계 테이블 도입 고민기: 설계 단계에서의 선택

nahud 2025. 9. 24. 18:42
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 = ?;

 

정합성 보장 방안도 고려했다

  1. 트랜잭션: 좋아요 등록/취소와 카운트 업데이트를 한 트랜잭션에서 처리
  2. 배치 보정: 주기적으로 실제 COUNT와 집계값 비교하여 보정
  3. 락 전략: 동시성 이슈 방지를 위한 적절한 락 사용

아직 남은 고민들

설계는 했지만 여전히 고민되는 부분들이 있다.

언제까지 집계 테이블을 써야 할까? 지금은 좋아요 수만 집계하지만, 나중에 조회수, 평점, 리뷰 수 등도 추가되면 집계 테이블이 많아질 수 있다. 어느 시점에서 다른 방식(예: 검색 엔진, 캐시)을 고려해야 할까?

정합성 이슈가 실제로 발생하면? 이론적으로는 트랜잭션으로 해결되지만, 실제 운영에서 예상치 못한 상황이 생길 수 있다. 모니터링과 복구 전략을 어떻게 세워야 할까?

배운 점

설계 단계에서 성능과 정합성 사이의 트레이드오프를 고민해 보는 경험이 값졌다.

완벽한 해답은 없다는 걸 깨달았다. 중요한 건 현재 상황에서 가장 합리적인 선택을 하고, 그 선택의 이유를 명확히 하는 것이다.

집계 테이블 도입은 현재 요구사항과 예상 트래픽을 고려했을 때 가장 현실적인 선택이었다. 나중에 더 좋은 방법을 찾게 되면 그때 다시 리팩토링하면 된다.

설계는 정답을 찾는 게 아니라, 합리적인 근거를 가진 선택을 하는 과정이라는 걸 다시 한번 느꼈다.