동시성 처리 (낙관적 락, 비관적 락)

JUNO LIM·2023년 5월 21일

Spring

목록 보기
9/9

공부를 위해 일부러 동시성 문제가 발생하는 로직을 구성해보았다.

포스트 테이블에 전체 좋아요 수 컬럼을 추가하여 사용자가 포스트에 좋아요를 누르면
좋아요 테이블에 insert 쿼리와 포스트 테이블의 전체 좋아요 수를 +1 하는
update 쿼리를 발생시키는 로직이다.

낙관적 락

낙관적 락은 버전을 가지고 동시성 제어를 하는 방법으로 @Version 어노테이션을
충돌이 일어날 것 같은 컬럼에 붙여서 사용한다.
버전이 달라 충돌이 발생한다면 OptimisticLockException 가 발생하게 되고
이 때 적절한 처리를 해주면 된다.

Post

@Version
private Long totalLikeCount;

public void addLikeCount() {
    totalLikeCount++;
}

LikesService

@Transactional
public void registerPostLike(Post post, UserAccount userAccount) {
    likesRepository.save(Likes.of(userAccount, post));

    post.addLikeCount();
    postRepository.flush();     
}

git bash를 사용해서 curl 명령어로 31명의 사용자가 동시에 좋아요 요청을 보내도록 해보았다.

[Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

충돌이 발생하여 ObjectOptimisticLockingFailureException이 발생하였다.
예외를 잡아서 적절하게 처리해 주면 되지만 이 로직은 항상 충돌이 발생하기 때문에 낙관적 락보다는 테이블에 직접 락을 거는 비관적 락이 더 효율적일 듯하다.

비관적 락

DB의 Shared Lock, Exclusive Lock을 이용하여 DB 레코드를 제어하는 방법이다.

전체 좋아요 수를 업데이트 하기 위해서 먼저 해당 포스트의 select 쿼리가 진행되고 그 다음 update 쿼리가 진행되기 때문에 Exclusive lock (배타적 잠금)을 사용해서 해당 트랜잭션이 완료될 때까지 해당 테이블 혹은 레코드(row)를 다른 트랜잭션에서 읽거나 쓰지 못하게 하였다.

PostRepository

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(value = "select p from Post p where p.id=:postId")
Post selectPostWithLock(Long postId);

PESSIMISTIC_WRITE 는 Exclusive Lock 이고 version 필드를 사용하지 않는다.

@Lock 어노테이션은 select 쿼리에 적용할 수 있고
해당 포스트를 찾는 순간 부터 업데이트가 완료될 때까지 하나의 트랜잭션으로 묶여있기 때문에 포스트 select 쿼리에 락을 걸어두어도 해당 트랜잭션이 종료될 때까지 쓰기 락은 적용된다.

똑같이 curl 명령어를 사용해서 동시에 요청을 보내보았다.

하나의 트랜잭션이 시작되면 락이 걸리기 때문에 충돌없이 모든 요청이 수행된 것을 볼 수 있다.

하지만 이 방법의 문제는 포스트 레코드에 쓰기 락이 걸리기 때문에 좋아요 요청을 수행하면 해당 포스트에 어떠한 작업도 수행할 수 없다는 것이다. 따라서 전체 좋아요 수 컬럼을 아예 삭제하고 필요할 때마다 카운트 쿼리를 수행하는 것이 훨씬 효율적일 것 같다는 생각이 든다.

profile
개발개발개발자

0개의 댓글