[SPOT] 분산 환경에서의 좋아요 정합성 99.9% 보장 및 성능 개선 시도

김민석·2025년 9월 19일
1

SPOT

목록 보기
10/11
post-thumbnail

현재 SPOT 출시 이후, 각 파트가 아쉬웠던 부분을 보완하기 위해 전체적인 서비스 리팩토링을 진행하고 있다. 개발 당시에는 당장 기능이 제대로 작동 하는것에만 집중을 해서 개발을 진행했기 때문에 스팟이 성공하여 많은 사용자를 유치하게 된다면 문제가 될 부분이 많다. 이번 리팩토링 기간을 계기로 그런 부분들을 보완하고자 한다.

우선 먼저 단일 서버에서 문제를 해결해보고, 이를 분산 서버에서도 적용할 수 있을지 확인해 볼 것이다.

성능 이슈

제일 먼저 개선해본 부분은 ‘좋아요’, ‘댓글 수’, ‘스크랩 수’ 같은 메타 데이터 조회 로직이다.
현재는 모든 매핑 테이블을 조인해서 레코드 개수를 카운트한 뒤 응답하고 있다.

정확하긴 하지만, 데이터가 많아질수록 불필요한 연산이 너무 많다.
게시글 조회 때마다 매핑 테이블을 조인해야 하니, 데이터가 쌓일수록 성능 문제는 확실히 발생할 수밖에 없다.

아래는 실제로 단일 서버-단일 DB 환경에서 게시글 200만 개, 좋아요 데이터 30만 개를 대상으로 돌려본 결과다.

구분평균 응답 시간(ms)P95(ms)P99(ms)
매핑 카운트 테이블2514.403041.823084.82

개선 방안

이를 개선하기 위해서는 매번 좋아요 갯수를 조인해서 세는 것이 아닌, 미리 정확한 값을 저장해두고 이를 가져오는 방식을 적용해야 한다고 판단했다.

게시글 테이블 내 카운트 컬럼

제일 먼저 떠올린 방법은 게시글 테이블 내 카운트 컬럼을 추가하는 방법이었다. 하지만 하나의 트랜잭션이 좋아요 수를 업데이트 하기 위해 해당 레코드에 락을 걸어버리면 병목 현상이 발생할 수 있다.

만약 내가 작성한 게시글이 인기가 많아져서 많은 사람이 좋아요를 누르고 있다고 가정하자. 이 상황 속 많은 트랜잭션이 내 게시글 레코드에 좋아요 수를 증가 시키기 위해 락을 점유하고 있다면, 나는 해당 게시글을 수정하려 해도 병목 현상이 발생할 수 있다.

게다가 게시글과 좋아요 수의 변경은 Lifecycle이 다르다. 게시글은, 게시글을 작성한 사용자가 쓰기 작업을 수행하고, 트래픽은 상대적으로 적다. 반면 좋아요 수는, 게시글을 조회한 사용자들이 쓰기 작업을 수행하고 트래픽은 상대적으로 많다.

즉, 서로 다른 주체에 의해서 레코드에 락이 잡힐 수 있는 것이다. 게시글 쓰기와 좋아요 수 쓰기는 사용자 입장에서 독립적으로 수행되는 기능이다.그런데 각 기능이 서로 영향을 끼칠 수 있는 것이다.

별도의 카운트 테이블 분리

그렇다면 카운트 컬럼을 별도의 테이블로 분리하면 위와 같은 문제를 해결할 수 있다. 좋아요 수 같은 메타 데이터는 직접적으로 게시글 테이블에 접근 하지 않아도 되고 카운트 테이블에서 작업을 처리하면 된다.

또한 좋아요 생성으로 인한 쓰기 작업이 게시글 테이블에 영향을 주지 않기 때문에 읽기 성능 향상도 극대화할 수 있다는 장점 역시 존재했다.

정합성 보장

카운트를 분리하면 실제 좋아요 데이터와 카운트 값이 어긋날 수 있다.
이걸 방지하려면 DB 트랜잭션 안에서 post_like insert + 카운트 증가를 하나로 묶어 원자성을 보장해야 한다.

동시성 문제

동시성 문제 역시 충분히 발생할 수 있다.

동시에 많은 회원이 하나의 게시글에 좋아요를 누르게 된다면, 좋아요 수를 업데이트 하는 과정에서 동시성 문제가 발생한다.

아래는 단일 서버에 동시에 3,000건 요청을 보낸 결과다.

구분최종 좋아요 수(개)성공률평균 응답 시간(ms)P95(ms)
락 X35211.7%120.36188.59

총 3,000개의 요청을 보냈지만, 카운트 테이블에서 보이는 최종 좋아요 수는 352개에 불과했다.

해결 방법들

비관적 락

  • 읽는 순간부터 DB 락을 건다.
  • 정합성은 확실하지만 성능이 떨어지고 데드락 위험도 있다.

낙관적 락

  • 락을 안 걸고 버전으로 충돌을 감지한다.
  • 성능은 좋지만 충돌 시 재시도 로직 필요하다
  • 재시도 없으면 정합성 깨지고, 재시도 있으면 성능이 급격히 떨어진다.

원자적 쿼리

  • update post_stats set like_count = like_count + 1 where post_id = ?
  • 단일 SQL로 원자적 처리.
  • 락 범위 최소, 성능 빠름.
  • 단순 증감 처리에는 가장 적합.

결론

단일 서버 테스트 결과

구분최종 좋아요 수(개)성공률평균 응답 시간(ms)P95(ms)
원자적 처리3000100%131.88189.46
비관적 락3000100%233.02380.69
낙관적 락 (재시도 O)230076.7%320.13892.22
낙관적 락 (재시도 X)34311.4%188.16390.47
락 X35211.7%120.36188.59

원자적 쿼리가 제일 안정적이고 빠르다는 게 확인됐다.
추가로 member_id + post_id 유니크 제약조건을 걸어 중복 좋아요도 DB 레벨에서 막았다.

낙관적 락 재시도는 최대 3회까지 시도하도록 설정했다.


분산 환경 테스트

마지막으로, 단일 DB + Spring 인스턴스 3개 + Nginx 로드밸런싱 환경에서 테스트를 진행했다. 단일 서버 환경과 달리, 멀티 인스턴스 환경에서도 정합성이 보장되는지 확인하는 게 목적이었다.


방식별 성능 비교

먼저, JPA 더티 체킹 + 비관락 방식과 원자적 쿼리 방식을 비교했다.

방식요청 수평균(ms)P95(ms)P99(ms)
JPA 더티체킹 + 비관락500366.32575.32589.41
1000600.881021.701055.48
원자적 쿼리 (단일 UPDATE)500242.55414.89426.53
1000319.69576.92666.17

정합성은 당연히 보장됐고, 분산 환경에서도 원자적 쿼리 방식이 가장 효율적이라는 걸 확인할 수 있었다. 락 범위를 최소화하면서도 안정적인 성능을 보여줬다.


5,000건 동시 요청 부하 테스트

또한, 좋아요 정합성 보장이 멀티 인스턴스 + 로드밸런서 환경에서도 유지되는지 검증하기 위해 부하 테스트를 추가로 진행했다.

  • 총 5,000건 이상의 요청을 동시에 발송
  • (postId, memberId) 조합을 유니크하게 생성 → 중복 충돌 방지
실행총 요청성공(2xx)실패(500)평균(ms)P95(ms)P99(ms)
15,0004,9982384.721,249.411,503.05
25,0004,98812368.961,178.391,278.41
35,0004,9919352.171,131.441,420.02
45,0004,98515349.461,048.431,127.19
55,0005,0000349.461,048.431,127.19

실패한 요청(HTTP 500)은 로그 확인 결과 전부 네트워크/리소스 한계로 인한 커넥션 리셋/타임아웃이었다. 즉, 비즈니스 로직 차원에서 정합성이 깨진 경우는 없었다.


결론

분산 환경에서도 좋아요 기능은 정합성을 보장했고, 5,000 동시 요청 시에도 성공률 99.9% 이상을 기록함을 확인했다.

profile
경험하며 성장하는 개발자 지망생

0개의 댓글