
좋아요 취소 기능 테스트 중에 발생한 문제이다.
1.20개의 스레드가 존재 -> 20명이 동시에 좋아요를 취소함
@OneToMany(mappedBy = "playListArticle",orphanRemoval = true,cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private List<PostLike> likeList;
@Test
@DisplayName("좋아요 동시에 취소했을 때")
public void concurrent() throws InterruptedException {
int threadCnt = 20;
ExecutorService executorService = Executors.newFixedThreadPool(threadCnt);
CountDownLatch startLatch = new CountDownLatch(1); // 동시 출발 신호
CountDownLatch doneLatch = new CountDownLatch(threadCnt);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
// When - 20명 동시에 좋아요 취소
for (int i = 0; i < threadCnt; i++) {
final Member member = members.get(i);
executorService.submit(() -> {
try {
startLatch.await(); // 모든 스레드 대기
playListArticleService.decreaseLike(articleId,member.getEmail());
successCount.incrementAndGet();
} catch (Exception e) {
failCount.incrementAndGet();
e.printStackTrace();
} finally {
doneLatch.countDown();
}
});
}
startLatch.countDown(); // 20개 스레드 동시 출발
doneLatch.await(); // 모든 스레드 완료 대기
executorService.shutdown();
// Then
PlayListArticle result = playlistArticleRepository.findByIdWithPessimisticLock(articleId).orElseThrow();//비관적락 적용
System.out.println("초기 좋아요 수: 20");
System.out.println("성공한 취소 수: " + successCount.get());
System.out.println("실패한 취소 수: " + failCount.get());
System.out.println("최종 좋아요 수: " + result.getLikeCnt());
assertThat(result.getLikeCnt()).isGreaterThanOrEqualTo(0); // 음수 방지
assertThat(result.getLikeCnt()).isEqualTo(0); // 20명 모두 취소했으므로 0
assertThat(result.getLikeList()).isNull(); // 좋아요 목록도 비어있어야 함
}
초기 좋아요 수: 20
성공한 취소 수: 0
실패한 취소 수: 20
최종 좋아요 수: 20
좋아요 누른 사람: [com.example.FifthSpring.model.PostLike@7b2637fa, com.example.FifthSpring.model.PostLike@5fea6cdb, com.example.FifthSpring.model.PostLike@5dfb0e1e, com.example.FifthSpring.model.PostLike@2f539c9b, com.example.FifthSpring.model.PostLike@6580f76c, com.example.FifthSpring.model.PostLike@6980d3b3, com.example.FifthSpring.model.PostLike@6c8eab63, com.example.FifthSpring.model.PostLike@5f0a452d, com.example.FifthSpring.model.PostLike@3dd3f237, com.example.FifthSpring.model.PostLike@52a1a375, com.example.FifthSpring.model.PostLike@4bedcff1, com.example.FifthSpring.model.PostLike@4643d88c, com.example.FifthSpring.model.PostLike@64a3d839, com.example.FifthSpring.model.PostLike@3b4675d3, com.example.FifthSpring.model.PostLike@1e71b70d, com.example.FifthSpring.model.PostLike@46ae8bf7, com.example.FifthSpring.model.PostLike@3c36f10b, com.example.FifthSpring.model.PostLike@3456e3b3, com.example.FifthSpring.model.PostLike@270cf3eb, com.example.FifthSpring.model.PostLike@15cf25ce]
PostLike 모델이 삭제되지 않았음을 알 수 있다.
왜 이런 문제가 발생하는지 Service 계층의 코드를 살펴보았다.
@Transactional
public PlayListDto decreaseLike(Long id,String email) {
PlayListArticle targetPost = playlistArticleRepository.findByIdWithPessimisticLock(id).orElseThrow(); //비관적 락 적용
playlistArticleRepository.decreaseLike(id);
likeRepository.deleteByPlayListArticleIdAndMember(id,memberRepository.findByEmail(email).orElseThrow());
return mapToPlayListDto(targetPost);
}
public interface PlaylistArticleRepository extends JpaRepository<PlayListArticle,Long> {
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("Select p from PlayListArticle p where p.id = :id")
@Transactional
Optional<PlayListArticle> findByIdWithPessimisticLock(@Param(value = "id") Long id); //비관적 락을 통해 cnt 문제 해결
@Modifying
@Query("Update PlayListArticle p SET p.likeCnt = p.likeCnt - 1 where p.id = :articleId")
void decreaseLike(@Param(value = "articleId") Long articleId);
}
public interface LikeRepository extends JpaRepository<PostLike,Long> {
@Transactional
void deleteByPlayListArticleIdAndMember(@Param(value="id") Long id, @Param(value="member") Member member);
}
public interface MemberRepository extends JpaRepository<Member,Long> {
Optional<Member> findByEmail(String email);
}
서비스 계층의 접근 순서는 PlaylistArticleRepository->likeRepository->memberRepository이다.
1. PlaylistArticleRepository에서는 findByIdWithPessimisticLock는 비관적 락을 적용하여 정합성을 유지하도록 하였다.
2.DB에서 삭제했지만 영속성 컨텍스트에 과거에 삭제된 데이터가 남아있었다.
→ @Modifying(clearAutomatically = true, flushAutomatically = true) 추가
→ 테스트에서 em.clear() 후 재조회
public interface LikeRepository extends JpaRepository<PostLike,Long> {
@Transactional
@Modifying(clearAutomatically = true, flushAutomatically = true) // 1차 캐시 초기화
@Query("Delete FROM PostLike p WHERE p.playListArticle.id = :id AND p.member.id = :memberId")
void deleteByPlayListArticleIdAndMember(@Param(value="id") Long id, @Param(value="memberId") Long memberId);
}
Could not initialize proxy [com.example.FifthSpring.model.Member#1] - no session와 같은 에러가 있었다. return PlayListDto.builder().id(playListArticle.getId()).userEmail(playListArticle.getMember().getEmail()).latitude(playListArticle.getLatitude()).longitude(playListArticle.getLongitude()).songList(songList).tagList(tagList).likeList(postLikeList).created(playListArticle.getCreated()).updated(playListArticle.getUpdated()).address(playListArticle.getAddress()).viewCnt(playListArticle.getViewCnt()).likeCnt(playListArticle.getLikeCnt()).build();
->여기서 no session의 오류가 발생했다.
userEmail(playListArticle.getMember().getEmail())
PlayListArticle을 살펴보면
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="member_id") private Member member;
fetch가 Lazy로 연결되어있다. 지연 로딩이 발생하여 NO SESSION이 된 것이다. 우선 지연로딩을 제거하여 SESSION문제를 해결했다.

영속성 컨텍스트를 고려하지 않고 DB에만 쿼리가 반영된다.영속성 컨텍스트에 있는 1차 캐시가 엔티티를 캐싱하게 된다. 이는 변경이 실행된 후의 실제 db와 같지 않은 값을 가져올 수 있다.@Modifying에서 제공하는 clearAutomatically를 이용해 영속성 컨텍스트를 clear하게 한다.flushAutomatically는 쿼리가 실행 되기 전에 지연 저장소의 쿼리를 Flush하게 한다.LOCK을 걸어서 다른 Transaction에서 데이터를 접근하지 못하게 한다.상호배제: 한 번에 한 개의 프로세스만이 공유자원을 사용점유대기: 프로세스가 할당된 자원을 가진 상태에서 다른 자원 대기비선점: 프로세스가 작업을 마친 후에 자원을 자발적으로 반환하기까지 대기순환 대기: 프로세스의 자원 점유 & 점유된 자원의 요구관계가 원형을 이루며 대기 -> 순환적으로 요구하는 자원을 서로가 가지고 있음