좋아요 수 설계는 아래의 내용을 전제로 한다.
- 좋아요 수는 전체 개수를 실시간으로 빠르게 보여줘야 한다.
좋아요 설계 방향
좋아요 수의 전체 개수를 실시간 조회(count)하는데 큰 비용이 든다면, 좋아요가 생성/삭제될 때마다 미리 좋아요 수를 갱신하는 방법이 있다. 좋아요 테이블의 게시글 별로 데이터 개수를 미리 하나의 데이터로 비정규화해두는 것이다.
좋아요 수를 설계하기 위해 좋아요 수의 데이터 특성을 살펴봐야 한다. 이 데이터는 아래와 같은 특성을 가지고 있다고 가정한다. (요구 사항에 따라 적절히 판단)
- 쓰기 트래픽이 비교적 크지 않다.
- 데이터의 일관성이 비교적 중요하다.
쓰기 트래픽이 크지 않고, 데이터 일관성이 중요하다면 RDB의 트랜잭션을 고려해볼 수 있다. 좋아요 테이블의 데이터 생성/삭제와 좋아요 수 갱신을 하나의 트랜잭션으로 묶는 것이다.
좋아요 수를 어디에서, 어떻게 관리할 것인가?
1. 게시글 테이블에 좋아요 수 컬럼 추가
좋아요 수는 게시글과 1:1 관계의 데이터이기 때문에, 게시글 테이블에 좋아요 수 컬럼을 추가하는 것을 고려해볼 수 있다. 하지만 게시글 테이블에 좋아요 수 컬럼을 추가하여 갱신하는 것은 제약이 발생할 수 있다.
게시글 테이블에 좋아요 수 컬럼을 비정규화하는 것은 Record Lock으로 인해 제약이 발생할 수 있다. 왜냐하면 게시글과 좋아요 수 변경은 LifeCycle이 다르기 때문이다. LifeCycle이 다른 근거는 아래와 같다.(주체, 트래픽)
- 게시글은
- 게시글을 작성한 사용자가 쓰기 작업을 수행한다.
- 트래픽이 상대적으로 적다.
- 좋아요 수는
- 게시글을 조회한 사용자들이 쓰기 작업을 수행한다.
- 트래픽이 상대적으로 많다.
게시글과 좋아요 수는 서로 다른 주체에 의해 Record Lock이 잡힐 수 있다. 즉, 작성자에 의한 게시글 쓰기와 조회자에 의한 좋아요 수 쓰기는 사용자 입장에서 독립적으로 수행되는 기능이다. 그렇기 때문에, 서로 다른 두 주체가 서로의 쓰기 작업에 영향을 끼칠 수 있다.
따라서, 게시글과 좋아요 수는 1:1 관계이지만 독립적인 테이블로 분리할 필요가 있다.
2. MSA 구조에서 게시글 서비스에 좋아요 수 테이블 추가
msa 구조를 전제로 아래와 같은 사항을 고려하고 있다.
- 각 서비스 별로 독립적인 데이터베이스를 구성한다.
- 샤딩이 고려된 분산 데이터베이스를 사용한다.
- 좋아요와 좋아요 수 데이터 일관성을 위해 관계형 데이터베이스의 트랜잭션을 고려하고 있다.
트랜잭션은 보통 단일 데이터베이스 내에서 안정적으로 빠르게 지원한다. 분산된 시스템에서 트랜잭션을 지원하려면 분산 트랜잭션을 고려해볼 수 있다. 하지만 분산 트랜잭션은 상대적으로 느리고 복잡할 수 있다. 만약 게시글 서비스의 데이터베이스에 좋아요 수 테이블을 관리한다면, 좋아요 서비스와 분리된 서비스가 되기 대문에 트랜잭션 관리가 복잡해진다. 게시글과 좋아요 수가 1:1 관계이지만 좋아요 서비스가 있기 때문에 좋아요 수를 게시글 서비스가 아닌 좋아요 서비스의 데이터베이스에서 관리하는 것이 바람직해 보인다.
3. 좋아요 서비스의 데이터베이스에 좋아요 수 테이블 추가
좋아요 수 데이터는 어떻게 관리할 것인가?
분산 시스템을 가정하고, 데이터의 적절한 분산을 위해 데이터베이스의 샤딩을 고려하고 있다. 만약 좋아요와 좋아요 수가 물리적으로 다른 샤드에 있다면 분산 트랜잭션이 필요해진다. 따라서, 좋아요 수 테이블의 샤드키는 좋아요 테이블과 동일한 데이터(article_id)로 해야 한다.
이렇게 설계하면, 좋아요 생성/삭제와 좋아요 수 갱신을 하나의 트랜잭션으로 묶어서 처리할수 있다.
@Transactional
public void like(Long articleId, Long userId){
ArticleLike articleLike = ArticleLike.create(snowflake.nextId(), articleId, userId);
articleLikeRepository.save(articleLike);
int result = articleLikeCountRepository.increase(articleId);
if(result == 0){
ArticleLikeCount articleLikeCount = ArticleLikeCount.init(articleId, 1L);
articleLikeCountRepository.save(articleLikeCount);
}
}
@Transactional
public void unlike(Long articleId, Long userId){
articleLikeRepository.findByArticleIdAndUserId(articleId, userId)
.ifPresent(articleLike -> {
articleLikeRepository.delete(articleLike);
articleLikeCountRepository.decrease(articleId);
});
}
동시성 문제
좋아요와 좋아요 수를 하나의 트랜잭션에서 관리하도록 했다. 하지만 높은 쓰기 트래픽이 들어올 수 있는 상황에서는 동시성 이슈 또한 고려해야 한다. 여러 개의 요청이 1개의 좋아요 수 레코드를 동시에 수정하는 상황이 발생한다면 데이터가 유실 또는 중복될 수 있기 때문이다. 동시성 문제를 제어하기 위한 방법으로 비관적 락, 낙관적 락, 비동기 순처 처리를 고려해볼 수 있다.
1. 비관적 락(Pessimistic Lock)
- 데이터 접근 시 항상 충돌이 발생할 가능성이 있다고 가정한다 (비관적 관점)
- 데이터 보호를 위해 항상 락을 걸어 다른 트랜잭션 접근 방지
- 다른 트랜잭션은 락이 해제되기까지 대기
- 락을 오래 점유하고 있으며, 성능 저하 또는 deadLock 등으로 인한 장애 문제가 있을 수 있다.
- 구현방법 1
- 데이터베이스에 저장된 데이터를 기준으로 update문 수행
- update 문 수행 시점에 락을 점유
- 락 점유하는 시간이 상대적으로 짧다.
transaction start;
-- 데이터 삽입
INSERT INTO article_like
VALUES(...);
-- 좋아요 수 데이터 갱신
-- Pessimistic Lock 점유
update article_like_count
set like_count = like_count + 1
where article_id = {articleId};
-- Pessimistic Lock 해제
commit;
- 구현방법 2
- for update 구문으로 조회 결과에 대해 락을 점유하겠다고 명시
- 트랜잭션에 조회된 데이터를 기준으로 update문 수행
- 데이터 조회 시점부터 락을 점유
- 락 점유하는 시간이 상대적으로 길다.
- 데이터 조회한 뒤 중간 과정을 수행해야 하기 때문에, 락 해제가 지연될 수 있다.
transaction start;
-- 데이터 삽입
INSERT INTO article_like VALUES(...);
-- for update 구문으로 데이터 조회
-- 조회된 데이터에 대해서 Pessimistic Lock 점유
select *
from article_like_count
where article_id = {articleId}
for update;
update article_like_count
set like_count = {updatedLikeCount}
where article_id = {articleId}
-- pessimistic Lock 해제
commit;
2. 낙관적 락(Optimistic Lock)
- 데이터 접근 시 충돌 발생할 가능성이 없다고 가정(낙관적 관점)
- version 컬럼을 추가하여 데이터 변경 여부 추적
- 각 트랜잭션에서 version을 함께 조회
- where 조건에 조회된 version을 넣고, version을 증가
- 데이터 변경이 성공하면 충돌이 없음
- 데이터 변경이 실패하면 충돌 (다른 트랜잭션에서 version을 이미 증가시켰음을 의미)
- 데이터 변경 여부를 확인하여 충돌을 처리
- 데이터가 다른 트랜잭션에 의해 수정되었는지 확인
- 수정된 내역이 있으면 후처리 (rollback 또는 재처리)
3. 비동기 순차 처리
- 모든 상황을 실시간으로 처리하고 즉시 응답해줄 필요는 없다는 관점
- 요청을 대기열에 저장해두고, 이후 비동기로 순차적으로 처리할 수 있다.
- 게시글마다 1개의 스레드에서 순차적으로 처리하면, 동시성 문제도 해결할 수 있다.
- 락으로 인한 지연이나 실패 케이스가 최소화된다. 즉시 처리되지 않기 때문에 사용자 입장에서는 지연될 수 있다.
- 큰 비용이 든다.
- 비동기 처리를 위한 시스템 구축 비용
- 실시간으로 결과 응답이 안되기 때문에 클라이언트 측 추가 처리가 필요
- 이미 처리된 것처럼 보이게 하고, 실패 시 알림을 주는 방식 등 추가 작업이 필요
- 서비스 정책으로 납득이 되어야 한다.
- 데이터의 일관성 관리를 위한 비용
- 대기열에서 중복/누락없이 반드시 1회 실행 보장되기 위한 시스템 구축이 필요하다.
4. 비관적 락을 선택하기까지의 과정
- 비동기 순차처리 배제
- 좋아요 쓰기 트래픽이 이를 고려할 만큼 크지 않을 것으로 판단.
- 비관적 락 VS 낙관적 락
- 비관적 락은 락을 명시적으로 잡아야 하지만, 게시글 단위로 좋아요가 처리되기 때문에, 좋아요 쓰기 트래픽에서는 단일 레코드에 대한 잠깐의 락은 문제되지 않을 수 있다.
- 낙관적 락은 락을 잡지 않기 때문에 지연은 낮을 수 있지만, 애플리케이션에서 충돌 감지 시 추가적인 처리가 필요하다.
- 가장 간단한 방법으로, 비관적 락의 방법 1을 채택
REFERENCE
스프링부트로 직접 만들면서 배우는 대규모 시스템 설계 - 게시판| 쿠케 - 인프런 강의
현재 평점 4.9점 수강생 1,164명인 강의를 만나보세요. 대규모 데이터와 트래픽을 지탱하기 위한 시스템을, 스프링부트로 직접 만들면서 배워봅니다. 대규모 시스템 디자인, Microservice Architecture, Ev
www.inflearn.com
'프로그래밍_인강' 카테고리의 다른 글
| 조회수 - InMemoryDB, 분산락 (1) | 2025.09.04 |
|---|---|
| 게시글 목록 조회 - 페이지 번호 (with 인덱스) (3) | 2025.07.21 |
| Distributed Relational Database (1) | 2025.07.15 |