hyper-link(통합플랫폼)의 테이블 구조는 member와 content의 다대다 관계를 막고자 중간에 member_content테이블을 두었다. 그래서 좋아요 클릭시 member_content의 컬럼이 insert되고, content테이블의 like_count컬럼 값이 증가한다.
이를 바탕으로 좋아요 API는 memberContent 저장 후, content테이블의 like_count를 증가하는 로직으로 구현하였다. 구현 후에, 동시성 테스트를 진행하는데, 데드락이 발생했다.
찾아보니 외래키가 존재하는 자식 테이블의 insert, update, delete시 부모테이블의 존재를 확인하기 위해 해당 컬럼에 s-lock을 건다고 한다. 따라서 아래와 같은 상황이 발생한 것으로 예상된다.
각 스레드가 서로 x-lock 걸기위해 반대편의 s-lock이 끝나기를 기다리는 것이다.
따라서 위의 로직을 'insert-update' 순서에서 'update-insert' 순서로 변경하여 x-lock을 먼저 걸어주도록 했다.
수정 전 코드
@Transactional
public LikeClickResponse clickLike(Long memberId, Long contentId,
LikeClickRequest likeClickRequest) {
if (likeClickRequest.addLike()) {
return createLike(memberId, contentId);
}
return deleteLike(memberId, contentId);
}
private LikeClickResponse createLike(Long memberId, Long contentId) {
existLike(memberId, contentId);
Content foundContent = contentService.findById(contentId);
memberContentRepository.save(new MemberContent(memberId, foundContent, LIKE));
int likeCount = contentService.addLike(contentId); //변경 코드
return new LikeClickResponse(likeCount);
}
private LikeClickResponse deleteLike(Long memberId, Long contentId) {
...
}
수정후 코드
@Transactional
public LikeClickResponse clickLike(Long memberId, Long contentId,
LikeClickRequest likeClickRequest) {
if (likeClickRequest.addLike()) {
return createLike(memberId, contentId);
}
return deleteLike(memberId, contentId);
}
private LikeClickResponse createLike(Long memberId, Long contentId) {
existLike(memberId, contentId);
int likeCount = contentService.addLike(contentId); //변경 코드
Content foundContent = contentService.findById(contentId);
memberContentRepository.save(new MemberContent(memberId, foundContent, LIKE));
return new LikeClickResponse(likeCount);
}
private LikeClickResponse deleteLike(Long memberId, Long contentId) {
...
}
테스트 코드
@BeforeAll
void setup() {
...
}
@AfterAll
void delete() {
...
}
@Test
@DisplayName("여러명이 동시에 좋아요를 눌러도 의도한 값을 얻을 수 있다.")
void multiThreadLikeClickTest() throws InterruptedException {
int threadCount = 10;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
LikeClickRequest likeClickRequest = new LikeClickRequest(true);
for (int i = 0; i < threadCount; i++) {
final int index = i;
executorService.execute(() -> {
likeService.clickLike(members.get(index).getId(), content.getId(), likeClickRequest);
latch.countDown();
});
}
latch.await();
Content foundContent = contentRepository.findById(content.getId())
.orElseThrow(ContentNotFoundException::new);
assertThat(foundContent.getLikeCount()).isEqualTo(threadCount);
}