상황
프로젝트에서 실시간 콘텐츠 시청 세션 기능을 구현했다. 사용자가 영상 페이지에 들어오면 시청자로 집계되고, 나가면 집계에서 빠져야 한다. 이 숫자는 웹소켓을 통해 모든 사용자에게 실시간으로 브로드캐스팅되어야 했다.
문제점
프론트에서 콘텐츠를 불러올 때, 요구하는 dto에 watcherCount가 들어있다.
그래서 기존 코드는 콘텐츠 엔티티에 watcherCount 필드를 사용했다. 그래서 WatchingSessionService에서 watchingSession이 업데이트 될때마다 이 콘텐츠의 필드도 업데이트 하는 방식으로 했다:
contentRepository.incrementWatcherCount(content.getId());
contentRepository.decrementWatcherCount(content.getId());
public interface ContentRepository extends JpaRepository<Content, UUID>, ContentRepositoryCustom {
@Modifying
@Query("UPDATE Content c SET c.watcherCount = c.watcherCount + 1 WHERE c.id = :contentId")
void incrementWatcherCount(@Param("contentId") UUID contentId);
@Modifying
@Query("UPDATE Content c SET c.watcherCount = c.watcherCount - 1 " +
"WHERE c.id = :contentId AND c.watcherCount > 0")
void decrementWatcherCount(@Param("contentId") UUID contentId);
}
여기서 생각해보면, 사용자 한명한명 들어올 때마다 매번 디스크 기반의 DB write 작업을 수행해야 한다.
- 현재 @Transactional을 메서드에 붙히긴 했지만, 트랜잭션의 원자성(성공 아니면 롤백)을 보장할 뿐, 여러 스레드가 동시에 같은 데이터에 접근하는 동시성 문제까지는 해결해주지 못한다.
- 만약에 시청자 A, B가 0.0001초 차이로 동시에 입장했으면 lost update가 생길 수 있다.
- Transaction A: DB에서 현재 인원 조회 -> 10명
- Transaction B: DB에서 현재 인원 조회 -> 10명 (A가 아직 저장을 안 했으므로)
- Transaction A: 10 + 1 = 11명으로 저장 (UPDATE)
- Transaction B: 10 + 1 = 11명으로 저장 (UPDATE)
- 결과적으로 두 명이 들어왔으니 12명이 되어야 하는데, DB에는 11명만 남는다. A의 업데이트 내역이 B에 의해 덮어씌워져 사라진 것이다 (Lost Update)
- 이를 막으려면 비관적 락을 걸어야하는데, A가 끝날때까지 B는 대기하게 하는 것이다. 그러면 데이터는 정확해지지만 만약에 100명이 들어오게 되면 100명이 줄을 서게 되어서 성능이 급격하게 저하된다.
해결 - Redis
Redis는 구조적으로 이 문제를 완벽하게 회피한다.
- Single Thread: Redis는 한 번에 하나의 명령어만 처리한다. 아무리 많은 요청이 동시에 몰려도 내부적으로는 줄을 세워 하나씩 순서대로 처리한다.
- Atomic Operation: Redis의 INCR(증가) 명령어는 "값을 읽고 -> 더하고 -> 저장하는" 과정이 분리되지 않고 한 방에 일어난다.
- 이제 100명이 동시에 INCR을 날려도 1부터 100까지 순서대로 빠르게 처리해서 정확히 +100을 만들어낸다.
바뀐 것들
- 이제 Content 테이블의 watcher_count 컬럼은 제거되고 실시간 용도로 쓰지 않는다.
- Redis는 실시간 조회수를 관리한다.
private static final String COUNT_KEY_PREFIX = "watching:count:";
public Long increaseWatcherCount(UUID contentId) {
String key = COUNT_KEY_PREFIX + contentId;
// DB 조회 없이 메모리에서 즉시 +1
return redisTemplate.opsForValue().increment(key);
}
public Long decreaseWatcherCount(UUID contentId) {
String key = COUNT_KEY_PREFIX + contentId;
// 메모리에서 즉시 -1
return redisTemplate.opsForValue().decrement(key);
}
결과
- DB 부하 감소: 단순 조회수 갱신을 위한 UPDATE 쿼리가 완전히 사라졌다. DB는 이제 중요한 비즈니스 데이터(결제, 유저 정보 등) 처리에만 집중할 수 있게 되었다.
- 그 위워별도의 락(Lock) 구현 없이도 Redis의 원자적 연산 덕분에 100명이 동시에 들어와도 정확하게 +100이 카운팅된다.
'Codeit > 프로젝트' 카테고리의 다른 글
| [모두의 플리] ES 성능 테스트! (0) | 2025.12.09 |
|---|---|
| [모두의 플리] 트러블슈팅 - 실시간 시청 세션 동기화와 Race condition (0) | 2025.12.02 |
| Elastisearch 토이 프로젝트 만들어보기 (프로젝트를 위한 준비..) (0) | 2025.11.27 |
| [모두의 플리] 고도화 - redis를 이용한 분산환경 고려 (pub/sub) (0) | 2025.11.26 |
| [모두의 플리] 트러블 슈팅 - 웹소켓 환경에서 유저 정보 가져오기 (0) | 2025.11.26 |