reids

code++·2일 전

[Spring Boot] 게시글 조회수 및 좋아요 동시성 문제와 성능 개선 (Redis 도입기)
🚨 문제 인식 (Troubleshooting)
현재 개발 중인 커뮤니티 플랫폼에서 게시글 상세 조회와 좋아요 기능을 구현했습니다. 기능적으로는 정상 작동하는 MVP(Minimum Viable Product) 상태였지만, "대용량 트래픽이 발생한다면 이 코드가 버틸 수 있을까?"라는 의문이 들었습니다.

기존 코드를 분석해 본 결과, 실무 환경에서는 치명적일 수 있는 두 가지 문제점을 발견했습니다.

  1. 조회수(Views) 무한 새로고침 어뷰징 및 DB 부하
    기존의 조회수 증가 로직은 다음과 같이 아주 단순했습니다.

java

// As-Is: 기존 BoardService 로직
@Transactional
public PostResponse incrementViewCount(Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));

post.incrementViewCount(); // DB의 조회수 + 1

return new PostResponse(...);

}
문제점:

한 명의 유저가 F5(새로고침)를 100번 누르면 조회수가 그대로 100이 올라가는 어뷰징에 무방비했습니다.
유저가 게시글을 클릭할 때마다 무조건 DB에 UPDATE 쿼리가 날아갑니다. 트래픽이 몰릴 경우 DB 커넥션이 고갈되고 엄청난 병목 현상이 발생할 수 있습니다.
2. 좋아요(Likes) 동시성 문제 및 DB Lock
좋아요 기능 역시 PostLike라는 매핑 테이블을 두어 정석대로 구현했습니다.

java

// As-Is: 기존 BoardService 로직
@Transactional
public boolean toggleLike(Long postId, String username) {
Post post = postRepository.findById(postId)...
Member member = memberRepository.findByUsername(username)...
Optional existingLike = postLikeRepository.findByMemberAndPost(member, post);

if (existingLike.isPresent()) {
    postLikeRepository.delete(existingLike.get());
    post.decrementLikeCount(); // 취소 시 감소
} else {
    postLikeRepository.save(new PostLike(member, post));
    post.incrementLikeCount(); // 클릭 시 증가
}

}
문제점: 로직 자체는 흠잡을 데가 없지만, 동시성(Concurrency) 측면에서 위험합니다. 유명인의 게시물에 수백 명이 동시에 좋아요를 누르게 되면, 1번 게시글(Row)에 수많은 쓰레드가 동시에 UPDATE 쿼리를 날리게 됩니다. 이는 심각한 DB Lock 경합을 유발하여 서버 장애로 이어질 수 있습니다.

🛠 해결 방안: Redis 인메모리 캐시 도입 (To-Be)
관계형 데이터베이스(RDBMS)에 직접 쿼리를 때리는 기존 방식을 버리고, 응답 속도가 매우 빠른 Redis(인메모리 데이터 저장소)를 활용한 아키텍처로 전면 개편하기로 결정했습니다.

  1. 조회수 개편 (중복 방지 및 스케줄러 동기화)
    중복 방지: 사용자의 식별자(username 또는 IP)를 조합하여 view:post:{postId}:user:{identifier}라는 키를 생성합니다. 이 키에 TTL(만료시간)을 24시간으로 걸어두어, 하루 동안은 같은 글을 여러 번 봐도 조회수가 오르지 않도록 방어했습니다.
    Write-Back 패턴 (Batch): 조회수를 올릴 때 DB에 UPDATE를 치지 않고, Redis의 post:views:{postId} 카운터만 INCR 시킵니다. 그리고 Spring Scheduler를 이용해 5분마다 한 번씩 Redis에 쌓인 조회수를 긁어모아 DB에 벌크 업데이트(Bulk Update)를 치도록 설계했습니다. DB 부하가 획기적으로 줄어듭니다.
  2. 좋아요 개편 (Set 자료구조 활용)
    Redis Set 활용: 좋아요를 누른 유저 목록을 Redis의 Set 자료구조(post:likes:{postId})로 관리합니다. SISMEMBER 명령어로 중복 여부를 O(1) 속도로 파악하고, SADD/SREM으로 토글 처리를 합니다.
    실시간 렌더링 + 비동기 처리: 사용자가 버튼을 누르면 Redis에서 즉시 카운트(SCARD)를 올려 프론트엔드에 빠르게 응답(200 OK)을 내려줍니다. 그리고 무거운 DB Insert/Delete 작업은 @Async를 활용하여 백그라운드 스레드에서 비동기로 처리하여 사용자 경험(UX)을 극대화했습니다.
    💡 회고 및 결론
    MVP 단계에서는 "일단 돌아가는 코드"를 짰다면, 이번 리팩토링을 통해 "실무에서 트래픽을 견딜 수 있는 튼튼한 아키텍처"에 대해 깊게 고민해 볼 수 있었습니다.

단순히 비즈니스 로직을 구현하는 것을 넘어, DB 트랜잭션을 최소화하고 캐시 서버를 적재적소에 활용하는 것이 백엔드 개발자의 진짜 핵심 역량이라는 것을 깨닫는 귀중한 트러블슈팅 경험이었습니다. 앞으로 남은 동기화 스케줄러 로직 구현과 프론트 연동 테스트도 꼼꼼히 진행할 예정입니다! 🚀

profile
일상

0개의 댓글