
좋아요 요청을 구현하는 과정에서 발생하는 동시성 문제를 해결하는 과정에서 synchronized 를 사용할 때 Transactional 에너테이션과 함께 사용하면 동시성 제어가 되지 않는 것을 확인했다.
해당 글 링크 : [Spring] 좋아요 기능 동시성 제어
위 글에서 간단하게 설명하긴 했지만 보다 더 자세한 내용을 정리하기 위해 이 글을 작성하기로 했다.
@Transactional과 synchronized 동시 사용 시 동시성 제어가 되지 않는 이유아래 예제 코드에서는 addPostLike 메서드에 @Transactional과 synchronized를 적용해 동시성 제어를 시도하고 있다. 그러나 기대한 대로 작동하지 않으며, 동시성 문제를 완벽히 해결하지 못한다.
@Transactional
@Override
public synchronized PostResDto addPostLike(Long id) {
PostResDto post = postRepository.findPostByIdOrElseThrow(id);
int updatedRow = postRepository.addPostLike(id, post.getLikes() + 1);
if (updatedRow > 0) {
return postRepository.findPostByIdOrElseThrow(id);
} else {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Something went wrong");
}
}
위 코드에서 @Transactional과 synchronized를 함께 사용했지만, 아래의 이유로 동시성 문제가 발생할 수 있다.
@Transactional의 프록시 패턴과 synchronized의 인스턴스 동기화 문제@Transactional은 프록시 객체를 통해 트랜잭션 경계를 설정한다. 즉, @Transactional이 적용된 메서드는 원본 메서드를 감싸는 프록시 객체에서 호출된다.synchronized 키워드는 동일 인스턴스의 메서드를 호출할 때만 동기화를 보장하지만, 트랜잭션 프록시를 통해 호출될 경우 동일 인스턴스 동기화가 제대로 이루어지지 않는다.synchronized 블록의 락을 공유하지 않거나, 적절히 제어하지 못할 가능성이 생긴다. 따라서 동시성 제어가 실패할 수 있다.@Transactional의 기본 격리 수준은 READ_COMMITTED이다. 이 수준에서는 각 트랜잭션이 자신이 커밋한 데이터만 읽을 수 있다. 그러나 트랜잭션이 겹쳐 실행될 경우, 다른 트랜잭션이 동시에 같은 데이터를 읽고 수정하려고 할 수 있다.addPostLike를 동시에 호출하면 중간에 변경된 데이터가 다른 트랜잭션에 영향을 주지 않거나 반영되지 않을 수 있어 동시성 문제가 발생한다.@Transactional과 synchronized는 독립적으로 동작한다@Transactional은 메서드 호출의 트랜잭션 경계만 관리하며, 동기화나 쓰레드 안전성에 대한 제어 기능이 없다.synchronized는 쓰레드 동기화를 위해 인스턴스 내 메서드 실행을 제어하지만, 트랜잭션 격리나 커밋 타이밍에는 영향을 미치지 않는다.synchronized가 해당 메서드를 보호한다고 해도, 트랜잭션 내부의 작업 단위가 독립적으로 실행되기 때문에 두 메서드가 함께 동작할 때 제대로 된 동시성 제어가 이루어지지 않는다.동일한 addPostLike 메서드에 여러 쓰레드가 동시에 접근한다고 가정하면 다음과 같은 일이 발생할 수 있다:
addPostLike 메서드를 호출하고, 트랜잭션이 시작된다.synchronized가 인스턴스 동기화를 시도하지만, 프록시 객체가 다르거나 트랜잭션이 겹쳐지는 문제가 발생해 두 쓰레드가 동시에 작업을 진행한다.@Transactional과 synchronized로 해결되지 않는 동시성 문제는 다음과 같은 방식으로 해결할 수 있다:
이와 같은 방안을 통해 트랜잭션과 동기화 모두 안전하게 수행하는 방식으로 동시성 문제를 해결할 수 있다.