스프링 동시성 문제 - (2) 좋아요 기능 ,낙관적,비관적,Named 락

이진우·2024년 4월 11일
0

스프링 학습

목록 보기
30/41

문제 코드

본 코드

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class LikePostService {
    private final PostRepository postRepository;
    private final LikePostRepository likePostRepository;
    private final PostLockRepository postLockRepository;


    //Post 에 대해 좋아요 를 누를 때를 위한 메서드
    @Transactional
    public void likePost(final Member currentMember,final Long postId){
            final Post post = findPost(postId);

            //post 의 작성자와 좋아요를 누르려는 사람의 ID 값이 같을 떄 예외
            if(post.getMember().getId() == currentMember.getId()){
                throw new NotFoundException(ErrorCode.MESSAGE_NOT_FOUND);
            }


            //이미 눌러져 있을 때 deleteLikePost 수행, 없다면 saveLikePost 수행
            likePostRepository.findByMemberIdAndPostId(currentMember.getId(), post.getId())
                .ifPresentOrElse(likePost -> {
                    deleteLikePost(likePost,post);
                },()->{
                    saveLikePost(currentMember,post);
                });
            
    }

    private Post findPost(final Long postId){
        return postRepository.findById(postId).orElseThrow(()->new NotFoundException(ErrorCode.MESSAGE_NOT_FOUND));
    }

    // 좋아요 누를 시 사용 메서드
    private void saveLikePost(final Member member,final Post post){
        LikePost lp= LikePost.builder()
            .post(post)
            .member(member)
            .build();

        likePostRepository.save(lp);
        
        post.increaseLikeCount();

   
    }

    //좋아요 한번 더 눌러 취소 시킬 때 사용 메서드
    private void deleteLikePost(final LikePost lp,final Post post){
        post.decreaseLikeCount();
       likePostRepository.delete(lp);
    }


}

위 코드를 짧게 설명하면 다음과 같다.
1. Post 와 Member 의 중간 테이블로 Member 가 좋아요 눌렀는지 확인 위한 LikePost 중간테이블
2. 좋아요 를 누르지 않은 Member 의 경우 좋아요를 누르면 saveLikePost 메서드 실행.

saveLikePost 메서드에서는 먼저 post 와 member 에 대한 외래키를 갖는 LikePost lp 를 저장한 이후
post.increaseLikeCount() 를 통해 post 의 필드 LikeCount 를 1증가.

를 수행한다.

우리는 여러 사람이 좋아요를 동시에 누를 경우 LikeCount 가 제대로 반영이 되는지 테스트를 해볼 것이다.

따라서 아래와 같은 테스트 코드를 작성한다.

테스트 코드

@SpringBootTest
public class LikePostThreadService {
    @Autowired
    private  LikePostService likePostService;

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private CommunityRepository communityRepository;

    @Autowired
    private LikePostLockService likePostLockService;

    @PersistenceContext
    private EntityManager entityManager;

    @PersistenceUnit
    private EntityManagerFactory emf;

    @Test
    @DisplayName("동시에 4명이 좋아요를 누를 때")
    //@Rollback(value = false)
    public void likeRequestSameTime() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(200);

        CountDownLatch countDownLatch = new CountDownLatch(4);
        Member member = Member.builder()
            .name("forPost")
            .nickName("forPost")
            .loginId("ddd")
            .password("kkk")
            .build();
        memberRepository.save(member);

        Community community = Community.of(member,"자유게시판");
        communityRepository.save(community);

        Post post = Post.builder()
            .title("조하")
            .contents("나빠")
            .isHideNickName(true)
            .isQuestion(false)
            .member(member)
            .community(community)
            .build();
        postRepository.save(post);

        Member tmpMember1 = Member.builder()
            .name("jinu")
            .loginId("dionisos")
            .password("kkkk")
            .nickName("ppp")
            .build();
        memberRepository.save(tmpMember1);

        Member tmpMember2 = Member.builder()
            .name("jinu")
            .loginId("dionisos")
            .password("kkkk")
            .nickName("ppp")
            .build();
        memberRepository.save(tmpMember2);

        Member tmpMember3 = Member.builder()
            .name("jinu")
            .loginId("dionisos")
            .password("kkkk")
            .nickName("ppp")
            .build();
        memberRepository.save(tmpMember3);

        Member tmpMember4 = Member.builder()
            .name("jinu")
            .loginId("dionisos")
            .password("kkkk")
            .nickName("ppp")
            .build();
        memberRepository.save(tmpMember4);

        System.out.println("----------------------");



        executorService.execute(()->{
            try{
                likePostService.likePost(tmpMember1,post.getId());
            }
            finally{
                countDownLatch.countDown();
            }
        });


        executorService.execute(()->{
            try{

                likePostService.likePost(tmpMember2,post.getId());
            }
            finally{
                countDownLatch.countDown();
            }
        });



        executorService.execute(()->{
            try{

                likePostService.likePost(tmpMember3,post.getId());
            }
            finally{
                countDownLatch.countDown();
            }
        });



        executorService.execute(()->{
            try{

                likePostService.likePost(tmpMember4,post.getId());
            }
            finally{
                countDownLatch.countDown();
            }
        });
        
        countDownLatch.await();

        
        Post findPost = postRepository.findById(post.getId()).get();
        System.out.println("여기"+findPost.getLikeCount());
        
    }

}

사전 작업이 길어서 그렇지 "---------" 이후만 보면 된다.
4명의 사용자가 동시에 한 게시물에 좋아요를 누르는 상황에 대한 테스트 코드이다. 이를 실행한다.

결과

그러면 아래와 같은 에러메시지가 발생한다.

Exception in thread "pool-2-thread-2" org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update post set contents=?,hide_name_sequence=?,is_hide_nick_name=?,is_question=?,like_count=?,reply_count=?,title=?,updated_at=? where post_id=?]; SQL [update post set contents=?,hide_name_sequence=?,is_hide_nick_name=?,is_question=?,like_count=?,reply_count=?,title=?,updated_at=? where post_id=?]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:262)
	

원인

엥? 데드락이 발생한다.

데드락은 위 경우 서로 다른 트랜잭션 들이 서로에 대해 필요한 자원에 대해 Lock 을 걸고 대기하는 상황에서 발생한다.

하지만 나는 어떠한 Lock 을 걸지도 않았는데 왜 Lock 을 들고 대기하는 현상이 생긴 것일까?

그 이유는 공식문서에서 찾을 수 있다.

외래키에 대한 SLock

외래키가 table 에 존재할때 그 record 에 대한 insert 혹은, update, delete 시 외래 키에 대해 SLock이란 것을 설정한다고 한다.

SLOCK이란?

특정 단위(테이블,레코드) 에 Lock 을 거는데
다른 트랜잭션에서 읽기는 가능하게 한다.
다른 트랜잭션 끼리 SLOCK 을 마음껏 걸어도 되는데
다른 트랜잭션에서 쓰기를 위한 XLock 이 미리 자원에 걸려있거나 혹은 XLock 이 다음에 오기 위해서는 Slock 이 먼저 끝나야 한다.

실제 SLOCK 이 걸리는지 확인

과연 그럴까??

디버깅을 통해 확인해보자!

위와 같이 BreakPoint 를 잡아주고

  @Test
   @DisplayName("동시에 4명이 좋아요를 누를 때")
   //@Rollback(value = false)
   public void likeRequestSameTime() throws InterruptedException {
       ExecutorService executorService = Executors.newFixedThreadPool(200);

       CountDownLatch countDownLatch = new CountDownLatch(4);
       Member member = Member.builder()
           .name("forPost")
           .nickName("forPost")
           .loginId("ddd")
           .password("kkk")
           .build();
       memberRepository.save(member);

       Community community = Community.of(member,"자유게시판");
       communityRepository.save(community);

       Post post = Post.builder()
           .title("조하")
           .contents("나빠")
           .isHideNickName(true)
           .isQuestion(false)
           .member(member)
           .community(community)
           .build();
       postRepository.save(post);

       Member tmpMember1 = Member.builder()
           .name("jinu")
           .loginId("dionisos")
           .password("kkkk")
           .nickName("ppp")
           .build();
       memberRepository.save(tmpMember1);

       Member tmpMember2 = Member.builder()
           .name("jinu")
           .loginId("dionisos")
           .password("kkkk")
           .nickName("ppp")
           .build();
       memberRepository.save(tmpMember2);

       Member tmpMember3 = Member.builder()
           .name("jinu")
           .loginId("dionisos")
           .password("kkkk")
           .nickName("ppp")
           .build();
       memberRepository.save(tmpMember3);

       Member tmpMember4 = Member.builder()
           .name("jinu")
           .loginId("dionisos")
           .password("kkkk")
           .nickName("ppp")
           .build();
       memberRepository.save(tmpMember4);

       System.out.println("----------------------");

       likePostService.likePost(tmpMember1, post.getId());

       Post findPost = postRepository.findById(post.getId()).get();
       System.out.println("여기"+findPost.getLikeCount());

   }

}

위와 같이 한 명이 좋아요를 눌렀을 때 그 때의 흐름을 볼 수 있도록 테스트 코드를 수정한다.

아직 저장하기 전 상황에는

아무런 락이 보이지 않는다.

하지만 저장 이후의 상황에서는

위와 같이 Like_post 의 외래키인 member 와 post 에 대해 S,REC_NOT_GAP 의 Lock_status 가 잡혀 있는 것을 볼 수 있다.

이는 post id 2에 대한 Gap Lock 이 아닌 레코드 단위로 S Lock 이 설정되었다는 것을 의미한다.

Update 시 XLock

Update Where 쿼리는 XLock 이 설정된다는 의미이다.

XLock 이란?

특정 Row 를 변경하고자 할 때 사용.
다른 트랜잭션은 읽기 작업을 위해 SLock을 걸거나, 다시 쓰기 작업을 위해 X-Lock 을 사용할 수 없다.

실제 X Lock 이 걸리는지 확인

이는 mysql work bench 에서 확인했다.

@Transactional 에서 정확히 어느타이밍에 변경감지가 일어나서 수정되는지 알기 힘들었기 때문이다.

아무튼 위 사진을 통해 update 시 post_id=1 에 대한 X Lock 이 잡히는 것 또한 확인했다.

데드락 흐름

사용자 A 의 트랜 잭션 1 -> LikePost 를 통한 외래키 Post 에 대한 SLock 획득 ->Post 에 대한 변경 감지 를 통한 수정으로 X Lock 을 얻고 싶지만 사용자 B의 Post 에 대한 Slock 획득으로 불가 .

사용자 B 의 트랜 잭션 2 -> LikePost 를 통한 외래키 Post 에 대한 SLock 획득 -> Post 에 대한 변경 감지 를 통한 수정으로 X Lock 을 얻고 싶지만 사용자 A의 Post 에 대한 Slock 획득으로 불가 .

실제로 변경 감지가 되기 전 마지막에도 즉 @Transactional 이 붙은 코드가 끝나기 전의 코드에도 외래키 post 에 대한 Slock 이 잡혀 있는 것을 볼 수 있다.

데드락을 없애기 위한 해결책

update 와 save 순서 바꾸기

그렇게 생각한 이유

현재 DeadLock 의 발생원인은 SLock 이 여러 트랜잭션 간에 동시에 잡고 있기 때문에 그 이유로 인해 다음 단계로 진행을 못하는 상황이다.

만일 SLock -> XLock 의 흐름이 아니라 XLock -> SLock 의 흐름으로 만들수 있다면 먼저 할당된 XLock 으로 다른 트랜잭션의 자원에 대한 읽기와 쓰기 모두 방지하여 데드락이 안걸리지 않을까라는 생각을 했다.

코드 수정

saveAndFlush 를 통해 먼저 Update 쿼리가 나가도록 즉 먼저 XLock 이 걸리도록 수정했다.

이렇게 하면 실제로 DeadLock 이 걸리지 않는 것을 확인할 수 있다.

우리가 원했던 대로

먼저 XLock 이 잡혔기에 나머지 쓰레드는 대기한다.

하지만 likePostRepository.save(lp) 를 통해 post가 SLock 으로 전환되는 것을 바랬지만 그렇지 않고 계속 XLock 으로만 존재했다.

문제점과 내가 생각하는 원인

데드락은 없어진 것을 확인했지만

데이터가 원하는 대로 나오지 않는다.

정상적인 상황이라면 4명이 좋아요를 눌렀기 때문에 위 사진에서 여기4 가 되어야 한다.

이에 대해 내가 생각하는 이유는

MySQL 은 기본적으로 Repeatable_READ 전략을 택한다.
이는 한 트랜잭션 내에서 어떤 데이터에 조회를 하고 사용을 하고 있었다면

다른 트랜잭션에서 그 데이터에 update 를 한 것을 commit 을 해도

트랜잭션의 동일성을 위해 undo 테이블의 있는 수정적인 것을 조회해 온다는 의미이다.

그렇기 때문에 update 를 통해 like count 를 변경해도 모든 트랜잭션이 like count 를 0에서 1로 변환하는 연산을 수행한다고 생각한다.

따라서 다른 방법을 찾아야 한다.

비관적 Lock 사용

그렇게 생각한 이유

아예 특정 post 에 대해 조회할 때 X- Lock 을 건다면?

코드 수정

확인을 위해

private void saveLikePost(final Member member,final Post post){
        LikePost lp= LikePost.builder()
            .post(post)
            .member(member)
            .build();



        likePostRepository.save(lp);
        post.increaseLikeCount();

    }

위 코드로 원복 시키고

애초에 post 를 찾아올 떄 XLock 을 걸고 찾아온다.

 private Post findPost(final Long postId){
       return postRepository.findWithPessimisticWriteById(postId).orElseThrow(()->new NotFoundException(ErrorCode.MESSAGE_NOT_FOUND));
   }

리포지토리에는

 @Lock(value = LockModeType.PESSIMISTIC_WRITE)
   @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout",value = "1000")})
   Optional<Post> findWithPessimisticWriteById(Long id);

를 건다.

  • PESSIMISTIC_WRITE를 통해 X Lock 을 설정한다. PESSIMISTIC_READ 를 통해 S Lock 을 설정할 수 있지만 이 특성을 사용하면 여전히 데드락이 발생한다(SLock 이므로!)
  • @QueryHints 를 통해 timeout 설정이 가능하다. 이를 통하면 이 예제에서와 상관없는 데드락의 위험성을 줄여줄 수 있다고 한다.
  • 이에 대해 테스트 코드를 실행시키면 정상적인 결과를 반환받을 수 있다.

    아까 순서를 바꾼 경우에서와 달리 이번에는 한 트랜잭션이 끝난 후 즉 like_count가 증가된 이후에 findPost 를 실행시킨 개념이 되므로 정상적이 된다.

    문제점 및 원인

    비관적 락으로 조회해온 post 에 대한 쿼리를 보면

     select
            p1_0.post_id,
            p1_0.community_id,
            p1_0.contents,
            p1_0.created_at,
            p1_0.hide_name_sequence,
            p1_0.is_hide_nick_name,
            p1_0.is_question,
            p1_0.like_count,
            p1_0.member_id,
            p1_0.reply_count,
            p1_0.title,
            p1_0.updated_at 
        from
            post p1_0 
        where
            p1_0.post_id=? for update

    이다. for update 는 XLock 때문에 붙었다. (S Lock은 for share)

    아무튼 for update 구문으로 레코드 레벨에 잠금을 건다. 이렇게 레코드 레벨에 락을 걸어버리면 해당 태스크의 트랜잭션 뿐만 아니라 다른 태스크들을 처리하기 위한 커넥션들도 락이 걸린 레코드에 접근하지 못한다는 단점이 있다고 한다.

    NamedLock 사용

    그렇게 생각한 이유

    NamedLock 은 락의 대상이 테이블, 레코드, 등 데이터베이스의 객체가 아니라 단순히 사용자가 문자열을 지정하면 그 문자열에 대해 Lock 을 획득하고 반납하므로 비관적 락의 단점을 없앤다. 따라서 이에 대해 실험해보았다.

    코드 수정

    일단 아까 비관적 Lock 을 사용하던 것을 원복한다.

    private Post findPost(final Long postId){
            return postRepository.findById(postId).orElseThrow(()->new NotFoundException(ErrorCode.MESSAGE_NOT_FOUND));
        }

    PostLock 에 대한 리포지토리를 따로 설정해준다.

    public interface PostLockRepository extends JpaRepository<Post,Long> {
       @Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
       void getLock(String key);
    
       @Query(value = "select release_lock(:key)", nativeQuery = true)
       void releaseLock(String key);
    
    }

    또한 이를 Service 코드에 이를 적용한다.

      @Transactional
        public void likePost(final Member currentMember,final Long postId){
            try {
                postLockRepository.getLock("likePost"+postId.toString());
                final Post post = findPost(postId);
    
                //post 의 작성자와 좋아요를 누르려는 사람의 ID 값이 같을 떄 예외
                if (post.getMember().getId() == currentMember.getId()) {
                    throw new NotFoundException(ErrorCode.MESSAGE_NOT_FOUND);
                }
    
                //이미 눌러져 있을 때 deleteLikePost 수행, 없다면 saveLikePost 수행
                likePostRepository.findByMemberIdAndPostId(currentMember.getId(), post.getId())
                    .ifPresentOrElse(likePost -> {
                        deleteLikePost(likePost, post);
                    }, () -> {
                        saveLikePost(currentMember, post);
                    });
    
          
            }finally {
                postLockRepository.releaseLock("likePost"+postId.toString());
            }
             System.out.println("hi");
        }

    이에 대해 테스트 코드를 실행하면

    데이터가 정확하지 않은 현상이 발생한다.

    이는 lock 을 해제한 이후에 트랜잭션이 커밋되어서 이러한 현상이 발생한다.
    예를 들어

    트랜잭션 1-> lock 잠금-> likepost 0에서 1로 증가 -> lock 해제 -> 트랜잭션 커밋

    동시에

    트랜잭션 2-> lock 획득 시도 -> 실패(트랜잭션 1에의 한 잠금) -> lock 이 해제 되자마자 post의 likeCount가 0인 상태의 post 를 읽고 1로 증가!

    이렇게 트랜잭션의 흐름이 되니
    갱신 분실의 형태가 일어난다.

    추가적으로

    releaseLock 을 할 때 post의 like count가 1증가하는 update 쿼리가 발생하는데 (Native 쿼리라 그런가 왜 그런지는 모르겠다)
    이렇다 할지라도 영속성 컨텍스트와 DB를 동기화 해주는 작업을 할 뿐
    커밋 되지 않았으므로

    다른 트랜잭션에서는 변함없이 post 의 likeCount 의 값을 0으로 인식한다.

    이러한 형태를 막으려면 단순히
    Lock을 해제하기 전에 트랜잭션을 커밋시키면 된다.

    해결 방법

    @Service
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class LikePostLockService {
       private final PostLockRepository postLockRepository;
       private final LikePostService likePostService;
       @Transactional
       public void LikePostWithLock(final Member currentMember,final Long postId){
           try{
               postLockRepository.getLock(postId.toString());
               likePostService.likePost(currentMember,postId);
           }
           finally{
               postLockRepository.releaseLock(postId.toString());
           }
       }
    
    }

    아예 Lock 에 대한 새로운 서비스 코드를 만들고,

     @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void likePost(final Member currentMember,final Long postId){
    

    로 새로운 Transactional 을 선언하고

    executorService.execute(()->{
                try{
                    likePostLockService.LikePostWithLock(tmpMember1,post.getId());
                }
                finally{
                    countDownLatch.countDown();
                }
            });
    

    이렇게만 수정해서 이에 대한 테스트 코드를 돌리면

    정상적으로 데이터가 정확한 것을 볼 수 있다.

    번외

    낙관적 락

    문제 코드

    이왕 여기까지 왔으니 낙관적 락도 한번 알아보자

    @Transactional
        public void addCount(Long communityId){
            Community community = communityRepository.findById(communityId).orElseThrow(()-> new NotFoundException(ErrorCode.MESSAGE_NOT_FOUND));
            community.addCount();
        }

    이번에는 연관관계 없이 편하게 update 문만 실행할 수 있도록 세팅했다.
    그냥 communtiy.add Count 를 하면 count 의 값이 하나씩 증가하는 단순한 코드이다.

     @Test
        @DisplayName("동시에 4명이 커뮤니티 count 증가시킬때 ")
        //@Rollback(value = false)
        public void likeCommunitySameTime() throws InterruptedException {
            ExecutorService executorService = Executors.newFixedThreadPool(200);
    
            CountDownLatch countDownLatch = new CountDownLatch(4);
            Member member = Member.builder()
                .name("forPost")
                .nickName("forPost")
                .loginId("ddd")
                .password("kkk")
                .build();
            memberRepository.save(member);
    
            Community community = Community.of(member,"자유게시판");
            communityRepository.save(community);
            
    
            System.out.println("----------------------");
    
    
    
    
            executorService.execute(()->{
                try{
                    communityService.addCount(community.getId());
                }
                finally{
                    countDownLatch.countDown();
                }
            });
    
    
            executorService.execute(()->{
                try{
                    communityService.addCount(community.getId());
                }
                finally{
                    countDownLatch.countDown();
                }
            });
    
    
    
            executorService.execute(()->{
                try{
                    communityService.addCount(community.getId());
                }
                finally{
                    countDownLatch.countDown();
                }
            });
    
    
    
            executorService.execute(()->{
                try{
                    communityService.addCount(community.getId());
                }
                finally{
                    countDownLatch.countDown();
                }
            });
    
            countDownLatch.await();
    
    
            Community findCommunity = communityRepository.findById(community.getId()).get();
            System.out.println("여기"+community.getCount());
    
        }

    아까와 비슷하게 동시에 4개의 addCount를 누른다면??
    정상적인 상황에서는 communtiy 의 count가 4개가 되어야 하지만

    1이 나온다.

    이는 멀티 쓰레드 구조에서 갱신 분실이 생기기 때문이다.

    트랜잭션 1 -> count가 0인 community 읽어옴 -> 0에서 1로 증가
    트랜잭션 2 -> count가 0인 community 읽어옴 -> 0에서 1로 증가

    이런식의 흐름이 된다.

    이를 낙관적 락으로 해결할 수 있다.

    낙관적 락 개념 및 코드 작성

    낙관적 락은 version 정보를 통해 데이터가 변경되었는지 변경이 안되었는지 확인한다.

    따라서 커뮤니티 클래스에 @Version 을 준다.

    @Entity
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    public class Community extends BaseEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "community_id")
        private Long id;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "member_id",updatable = false)
        private Member member;
    
        @Column(nullable = false)
        private String name;
    
    
        private Community(Member member,String name){
            this.member = member;
            this.name = name;
            this.count = 0;
        }
        public static Community of(Member manager,String name){
            return new Community(manager,name);
        }
    
    
        private int count=0;
    
        @Version
        private Long version;
    
        public void addCount(){
            count++;
        }
    
    
    }

    그리고 다른 Lock 과 달리 실제 낙관적 락은 실제로 데이터베이스에 Lock 이 걸리는 개념이 아니기 때문에 version 정보가 다를 때의 처리를 하는 방법을 사용자가 직접 작성해주어야 한다.

    @Service
    @RequiredArgsConstructor
    public class CommunityServiceFacade {
        private final CommunityService communityService;
    
        public void increaseCount(Long communityId) throws InterruptedException {
            while (true) {
                try {
                   communityService.addCount(communityId);
                    break;
                } catch (ObjectOptimisticLockingFailureException e) {
                    Thread.sleep(30);
                }
            }
        }
    }

    이제 부터 이 클래스의 increaseCount 를 통해 communityService 의 addCount 를 호출하고, 거기서 ObjectOptimisticLockingFailureException e 예외가 발생하면 잠시 쓰레드를 멈춘후 다시 시도하게 한다.

    벌써부터 단점이 보인다 .
    1)version 정보를 주어야 한다는 점
    2)정보가 업데이트 되었을 때의 예외 처리를 개발자가 직접 해주어야 한다는 점

    아무튼

    @Test
        @DisplayName("동시에 4명이 커뮤니티 count 증가시킬때 ")
        //@Rollback(value = false)
        public void likeCommunitySameTime() throws InterruptedException {
            ExecutorService executorService = Executors.newFixedThreadPool(200);
    
            CountDownLatch countDownLatch = new CountDownLatch(4);
            Member member = Member.builder()
                .name("forPost")
                .nickName("forPost")
                .loginId("ddd")
                .password("kkk")
                .build();
            memberRepository.save(member);
    
            Community community = Community.of(member,"자유게시판");
            communityRepository.save(community);
    
    
            System.out.println("----------------------");
    
    
    
    
            executorService.execute(()->{
                try{
                    communityServiceFacade.increaseCount(community.getId());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally{
                    countDownLatch.countDown();
                }
            });
    
    
            executorService.execute(()->{
                try{
                    communityServiceFacade.increaseCount(community.getId());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally{
                    countDownLatch.countDown();
                }
            });
    
    
    
            executorService.execute(()->{
                try{
                    communityServiceFacade.increaseCount(community.getId());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally{
                    countDownLatch.countDown();
                }
            });
    
    
    
            executorService.execute(()->{
                try{
                    communityServiceFacade.increaseCount(community.getId());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally{
                    countDownLatch.countDown();
                }
            });
    
            countDownLatch.await();
    
    
            Community findCommunity = communityRepository.findById(community.getId()).get();
            System.out.println("여기"+findCommunity.getCount());
    
        }

    이런 식으로 테스트 코드를 작성하면

    성공적으로

    좋아요 갯수가 4개가 되는 것을 볼 수 있다.

    위 경우 자동으로 LockModeType이 None 으로 세팅된다 .

    만약에 LockModeType을 Optimic 을 사용한다면

    수정할 때만 version 정보를 확인하는 None 과 달리
    조회할 때도 version 정보를 확인하기 때문에

    엔티티의 조회 시점부터 트랜잭션이 끝날 때 까지 다른 트랜잭션에 의해 변경되지 않음을 보장 하는 역할을 한다.

    낙관적 락 vs 비관적 락

    낙관적 락 장단점

    낙관적 락 장점
    1) 동시성 문제가 거의 일어나지 않은 경우에는 비관적 락보다 성능이 좋게 작동한다.

    단점
    1) @Version 을 사용해야 한다는 점
    2) 예외 처리를 개발자가 해야 한다는 점
    3) 동시성 문제가 많이 일어난다면 오히려 성능이 비관적 락보다 떨어질 수 있다는 점

    비관적 락 장단점

    비관적 락 장점
    1) 충돌을 방지하여 데이터의 일관성을 맞출 수 있다.
    2) 동시성 문제가 많이 일어날 시 낙관적 락보다 비관적 락이 성능이 더 좋다

    비관적 락 단점
    1) mysql 데이터 레코드 레벨 에 Lock 을 걸기 때문에 다른 일들을 처리하기 위한 트랜잭션도 해당 레코드를 접근할 수 없다.
    2) timeout 을 설정하지 않을 시 데드락이 발생할 수 있다.( 알아볼 필요가 있다)

    비관적 락 vs Named Lock

    Named Lock 은
    데이터베이스의 어떤 단위 예를 들면 레코드 , 테이블 , Row 등에 Lock 을 거는 개념이 아니라 사용자가 지정한 문자열에 의해 lock 이 걸리기 떄문에 동시성 문제가 발생하는 트랜잭션에 대해서만 따로 관리를 해줄 수 있다.

    profile
    기록을 통해 실력을 쌓아가자

    0개의 댓글