프로젝트

[DB] 좋아요 수 설계 및 동시성 문제 해결 방법

욘아리 2025. 3. 4. 14:05

좋아요 수 설계 고민과 어떤 식으로 진행했는지 해결 방법에 대해 작성해두려고 한다.

 

좋아요 수 설계

  • 좋아요 수는 실시간으로 빠르게 조회되어야 한다.
  • 좋아요가 생성/삭제될 때마다 미리 좋아요 수를 갱신하는 방식이 필요하다.
  • 좋아요 테이블에서 게시글별 좋아요 수를 미리 비정규화하여 저장하는 방안을 고려한다.

게시글 테이블에 좋아요 수 칼럼 추가?

장점

  • 게시글과 좋아요 수가 1:1 관계이므로, 비정규화가 어색하지 않다.
  • 데이터 조회 시 추가적인 조인이 필요 없어 성능적으로 유리할 수 있다.

문제점

  1. Record Lock 및 분산 트랜잭션 문제
    • 좋아요 수가 업데이트될 때마다 게시글 테이블의 동일한 행이 갱신되므로 동시성 제약이 발생할 수 있다.
    • 트래픽이 많아지면 Lock 경합이 심해져 성능 저하가 발생할 수 있다.
    • 분산 환경에서 트랜잭션을 관리하기 어렵다.
  2. 게시글과 좋아요 수의 트래픽 차이
    • 게시글의 수정 트래픽은 상대적으로 적다.
    • 좋아요 수의 갱신 트래픽은 상대적으로 많다.
    • 서로 다른 트래픽 특성을 가진 데이터를 동일한 테이블에서 관리하면 성능 이슈가 발생할 수 있다.

💡해결책 - 좋아요 서비스의 별도 테이블에서 관리

  • 좋아요 서비스의 데이터베이스에 별도의 좋아요 수 테이블을 만들어 관리한다.
  • 이를 통해 분산 환경에서도 트랜잭션 관리가 수월해지고, 게시글 테이블의 Lock 문제를 방지할 수 있다.

 

동시성 문제 해결

좋아요 수를 관리할 때, 동시 쓰기 요청이 발생하면 데이터 유실 및 무결성 문제가 발생할 수 있다. 이를 방지하기 위해 세 가지 방법을 고려할 수 있다.

 

비관적 락 (Pessimistic Lock)

  • 데이터 접근 시 충돌 가능성이 높다고 가정하고, 항상 락을 걸어 다른 트랜잭션의 접근을 방지한다.
  • 단점: 락을 오래 점유하면 성능 저하 및 Deadlock 문제가 발생할 수 있다.

구현 방법

  1. UPDATE문을 통한 갱신 (Record Lock 활용)
    • 데이터베이스의 현재 저장된 데이터를 기준으로 즉시 업데이트한다.
    • UPDATE 시점에서 락이 걸리므로, 충돌 방지 가능.
      transaction start;
      
      -- 좋아요 데이터 삽입
      insert into article_like
      values({article_like_id}, {article_id}, {user_id}, {created_at});
      
      -- 좋아요 수 데이터 갱신
      update article_like_count set like_count = like_count + 1
      where article_id = {article_id};
      
      commit;

  2. FOR UPDATE를 활용한 조회 후 갱신
    • SELECT 시점부터 락을 점유하므로, 동시 업데이트 방지 가능.
    • 단점: 락을 오래 유지하게 되어 성능이 저하될 수 있음.
      transaction start;
      
      -- 좋아요 데이터 삽입
      insert into article_like
      values({article_like_id}, {article_id}, {user_id}, {created_at});
      
      -- for update 구문으로 데이터 조회, 조회된 데이터에 대해서 비관적 락
      select * from article_like_count
      where article_id = {article_id} for update;
      
      -- 좋아요 수 데이터 갱신
      update article_like_count set like_count = {update_like_count}
      where article_id = {article_id};
      
      commit;
       

 

낙관적 락 (Optimistic Lock)

  • 충돌 가능성이 낮다고 가정하고, 변경 시점에 충돌을 감지하여 해결하는 방식.
  • 데이터 갱신 전후로 버전(version) 필드를 활용하여 충돌을 감지한다.
  • 충돌이 감지되면 롤백하거나 재시도를 수행.

구현 방법

  • 트랜잭션에서 데이터를 조회할 때 버전을 확인하고, 업데이트 시 이전 버전과 동일한 경우에만 갱신한다.
  • 동시에 여러 요청이 발생하면, 가장 먼저 완료된 트랜잭션만 업데이트에 성공하며, 나머지는 재시도해야 한다.
  • 단점: 충돌이 많아지면 재시도 횟수가 증가하여 성능 저하 가능.

 

비동기 순차 처리 (Async Processing)

  • 모든 좋아요 증가 요청을 비동기적으로 처리하여 동시성 문제를 방지하는 방식.
  • Kafka 같은 메시지 큐를 활용하여 좋아요 증가 요청을 큐에 적재한 후, 순차적으로 처리하는 방법.
  • 단점: 즉각적인 데이터 반영이 어려우므로, 실시간성이 중요한 경우 부적절할 수 있음.

구현 방법

  1. 좋아요 이벤트를 Kafka에 발행.
  2. 별도의 Consumer가 큐에서 메시지를 가져와 처리.
  3. 최종적으로 article_like_count 테이블을 갱신.

 

👉 좋아요 수를 효과적으로 관리하기 위해 좋아요 서비스의 별도 테이블에서 데이터를 관리하며, 동시성 문제를 방지하는 방법을 고려해야 한다.

  • 트랜잭션 충돌을 방지하기 위해 비관적 락, 낙관적 락, 비동기 처리 방법을 활용할 수 있다.
  • 상황에 따라 즉각적인 반영이 필요한 경우 비관적 락, 충돌 가능성이 적다면 낙관적 락, 대량 트래픽을 처리해야 한다면 비동기 큐 방식을 선택하면 된다.

 

그 중 학습을 위해 3가지 방법(비관적 락 2가지, 낙관적 락)을 직접 사용해 봤다.

테스트 결과

 

비관적 락 1 (pessimistic-lock-1)

update 문을 실행하는 시점에 락을 점유하여 다른 트랜잭션의 접근을 차단하는 방식

  • 3,000개의 동시 요청이 발생했지만, 데이터 유실 없이 정확하게 3,001(기존 + 3,000)이 나온다.
  • 하지만 성능은 매우 느려서 69초나 걸림 → 락을 걸고 해제하는 과정에서 병목이 생긴 것으로 보인다.

 

비관적 락 2 (pessimistic-lock-2)

for update 구문을 사용하여 조회 시점부터 락을 점유하는 방식.

  • pessimistic-lock-1보다 더 긴 시간 동안 락을 유지하기 때문에 속도가 더 느려짐(107초).
  • 데이터 정합성은 유지되었지만, 락 유지 시간이 길어져 성능이 더 나빠짐.

 

낙관적 락 (optimistic-lock)

  • 실행 시간은 비교적 빠른 30초로 비관적 락보다 훨씬 성능이 좋음.
  • 하지만 최종 좋아요 수가 361밖에 되지 않음 → 데이터가 유실됨.
    • 동시 쓰기 충돌이 발생하여 낙관적 락의 version 충돌로 인해 실패한 요청이 많음.
    • 충돌 후 재시도 로직을 구현하지 않으면 데이터 유실 가능성이 높음.

 

좋아요 수 저장과 동시성 문제 해결은 데이터 정합성과 성능 사이의 균형을 맞추는 것이 중요하다. 트래픽 규모와 서비스 요구사항을 고려해 적절한 방법을 선택해야 한다.