✨ 들어가며
유저가 게시글에 좋아요를 누르면 일정 수치 이상일 때 'HOT 게시판'으로 이동시키는 기능을 구현했습니다. 이 기능은 좋아요 수가 많을수록 더 많은 유저들에게 노출되도록 유도하는 핵심적인 UX입니다.
그런데 분명 좋아요 수가 3 이상인데도 HOT 게시판에 올라가지 않거나, 반대로 좋아요 수가 3 미만인데도 HOT 게시판에 남아있는 경우가 생겼습니다. 다수의 사용자가 동시에 좋아요를 누르거나 취소할 때 Race Condition이 발생했습니다.
🧨 기존 코드의 문제점
post.increaseLike();
if (post.getLikeCount() >= 3) {
moveToHotBoard(post);
}
post.decreaseLike();
if (post.getLikeCount() < 3) {
removeToHotBoard(post);
}
초기에는 좋아요 수는 Post 객체의 likeCount 필드로 관리됐고, 매 호출 시 증가 혹은 감소시켰습니다.
- 동시성 문제: 여러 사용자가 동시에 좋아요를 누르면 post.getLikeCount()는 정확하지 않을 수 있습니다. 예를 들어, 좋아요 수가 2일 때 두 사용자가 동시에 좋아요를 누르면, 둘 다 post.getLikeCount()가 2라고 보고 moveToHotBoard()를 호출하지 않았습니다.
- DB와 불일치: 좋아요 수는 별도 테이블 PostLike을 기반으로 계산되지만 이를 별도로 유지하는 필드를 수동으로 관리하고 있었기에 결국 Post 객체와 DB의 상태가 불일치할 수 있었습니다.
🚀 비관적 락을 사용한 개선
1. 비관적 락(Pessimistic Lock) 적용
동시성을 가장 확실하게 제어하기 위해 Post 조회 시 PESSIMISTIC_WRITE 락을 걸어 변경 중인 데이터를 다른 트랜잭션에서 읽지 못하게 했습니다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Post p where p.id = :id")
Optional<Post> findByIdWithPessimisticLock(@Param("id") Long id);
2. PostLike 저장/삭제 → countByPostId로 좋아요 수 계산
long likeCount = postLikeRepository.countByPostId(postId);
좋아요 수를 직접 증가시키지 않고, PostLike 테이블에서 카운트하여 정확한 수치를 기준으로 HOT 게시판 이동 여부를 판단하도록 했습니다.
3. 좋아요 트랜잭션 분리 및 원자화
- PostLike 삽입/삭제
- Post 비관적 락 조회
- postLikeRepository.countByPostId()로 현재 정확한 좋아요 수 조회
- 기준 수치 이상이면 moveToHotBoard() 호출 or 기준 수치 미만이면 removeFromHotBoard() 호출
Post post = postRepository.findByIdWithPessimisticLock(postId).orElseThrow(PostNotFoundException::new);
long likeCount = postLikeRepository.countByPostId(postId);
if (likeCount >= HOT_BOARD_THRESHOLD && !post.isHotBoard()) {
moveToHotBoard(post);
}
Post post = postRepository.findByIdWithPessimisticLock(postId).orElseThrow(PostNotFoundException::new);
long likeCount = postLikeRepository.countByPostId(postId);
if (likeCount < HOT_BOARD_THRESHOLD && post.isHotBoard()) {
removeFromHotBoard(post);
}
📊 효과는?
- Race Condition 제거
- 다수 사용자가 동시에 좋아요를 누르는 상황에서도 일관된 로직으로 HOT 게시판 진입 조건을 판단
- Pessimistic Lock을 사용해 게시글 데이터를 선점적으로 잠금 처리하여, 중복 이동이나 조건 무시 방지
- 정확한 좋아요 수 기반 판단
- Post 객체 내 증가형 likeCount 필드가 아닌, 항상 최신 상태의 PostLike 테이블을 직접 카운트하여 정확하고 실시간 반영된 수치 사용
- 이로 인해 좋아요 수 오류 및 동기화 문제 제거
- 트랜잭션 안정성 확보
- 좋아요 저장, 삭제, 좋아요 수 계산, HOT 게시판 이동을 동일 트랜잭션 내에서 처리하여 중간 실패나 일관성 깨짐 방지
- HOT 게시판 이동 여부 판단도 함께 묶여 비즈니스 로직의 신뢰성 강화
✅ 정리하며
좋아요 수는 단순해 보이지만, 동시성 문제가 숨어 있는 의외의 난관이었습니다 🥹
아무 고민 없이 단순히 수치를 증가시키는 로직만 두면 여러 사용자가 동시에 접근하는 상황에서 의도치 않은 버그가 쉽게 발생할 수 있었습니다.
👉🏻 이를 해결하기 위해 비관적 락(Pessimistic Lock) 을 도입하고, 좋아요 수를 PostLike 테이블에서 직접 계산하는 구조로 변경했습니다. 그 결과, 트래픽이 몰리는 상황에서도 일관성과 신뢰성을 유지할 수 있게 되었습니다.

'프로젝트 > Wedle' 카테고리의 다른 글
[자주 조회되는 데이터 Redis 캐싱] – 학사 캘린더 캐싱 (1) | 2025.04.09 |
---|---|
[JPA 성능 최적화] @ManyToOne(fetch = FetchType.LAZY) 와 Fetch Join 활용하기 (0) | 2025.04.08 |
[JPA 성능 최적화] 반복되는 save() 대신 saveAll()로 벌크 저장 (0) | 2025.04.08 |
[Spring Boot/Kafka/Stomp] 실시간 채팅 구현 – 7. 채팅 전송 및 조회 (0) | 2025.03.19 |
[Spring Boot/Kafka/Stomp] 실시간 채팅 구현 - 6. 채팅방 생성 (0) | 2025.03.19 |