@Transactional
public LikeResponseDTO toggleLikePost(LikeRequestDTO dto) {
Post post = postRepository.findById(dto.postId())
.orElseThrow(() -> new BadRequestException(POST_NOT_FOUND));
Member member = memberRepository.findById(dto.userId())
.orElseThrow(() -> new BadRequestException(MEMBER_NOT_FOUND));
Optional<PostLike> existingLike = postLikeRepository.findByMemberAndPost(member, post);
if (existingLike.isPresent()) {
// 이미 좋아요 되어있다면 → 좋아요 취소
postLikeRepository.delete(existingLike.get());
post.decreaseLikeCount();
return new LikeResponseDTO(post.getId(), post.getLikeCount());
} else {
// 좋아요 안 되어있다면 → 좋아요 추가
postLikeRepository.save(PostLike.of(member, post));
post.increaseLikeCount();
// LogUtil.loggingInteraction(LIKE, post.getId());
return new LikeResponseDTO(post.getId(), post.getLikeCount());
}
}
동시성 테스트
서비스 가입자 수는 60명. 이중 절반의 유저가 좋아요 / 좋아요 취소 요청을 반복해서 보내는 상황 가정했습니다.
Ramp-up으로 2분간 점진적 부하하고 이후 2분간 최대 부하를 지속했습니다.
30명의 유저가 10개의 포스트 중 하나로 좋아요 요청 / 취소 반복하는 상황을 설정했습니다.
테스트 결과
500 에러가 발생한 이유 : 요청이 여러 번 오는 조건 속에 동시성 문제 발생
ALTER TABLE likes
ADD CONSTRAINT uq_member_post UNIQUE (member_id, post_id);
Unique 키 제약조건을 추가하기 위해 Soft Delete 코드를 주석처리 했습니다.
기존 대비 전체적인 Internal Server Error가 줄었지만 완전히 문제가 해결되지는 않는 것을 확인할 수 있습니다.
로그 분석
SQLIntegrityConstraintViolationException & StaleObjectStateException
Deadlock
( 내용 추가 예정 )
기존 구조
변경 후 구조
장점 :
Redis는 단일 쓰레드로 동작하기에 동시성 문제를 어느 정도 해결해준다.
In memory DB로 빠르게 반영 가능하다.
DB 부하 분산해준다.
단점 :
동시성 문제를 완전히 해결하는 것은 아니다.
데이터 유실이 가능하다.
일관성 지연이 발생할 수 있다.
@Transactional
public LikeResponseDTO redisToggleLikePost(LikeRequestDTO dto) {
String postKey = buildPostKey(dto.postId());
String userKey = buildUserKey(dto.postId(), dto.userId());
String userSetKey = buildUserSetKey(dto.postId());
initLikeCountIfAbsent(dto.postId(), postKey); // 좋아요 수 캐싱
boolean hasLiked = TRUE.equals(redisTemplate.hasKey(userKey));
if (hasLiked) {
processUnlike(postKey, userKey, userSetKey, dto.userId());
} else {
processLike(postKey, userKey, userSetKey, dto.userId());
}
redisTemplate.opsForSet().add(TRACKED_POST_SET_KEY, String.valueOf(dto.postId()));
long likeCount = getLikeCount(postKey);
return new LikeResponseDTO(dto.postId(), likeCount);
}
실제로 동시성 문제를 회피하고 안정적으로 응답하는 것을 확인할 수 있습니다.