[Spring] Transactional 과 Synchronized

김민범·2024년 11월 10일

Spring

목록 보기
16/29


좋아요 요청을 구현하는 과정에서 발생하는 동시성 문제를 해결하는 과정에서 synchronized 를 사용할 때 Transactional 에너테이션과 함께 사용하면 동시성 제어가 되지 않는 것을 확인했다.

해당 글 링크 : [Spring] 좋아요 기능 동시성 제어

위 글에서 간단하게 설명하긴 했지만 보다 더 자세한 내용을 정리하기 위해 이 글을 작성하기로 했다.

@Transactionalsynchronized 동시 사용 시 동시성 제어가 되지 않는 이유

아래 예제 코드에서는 addPostLike 메서드에 @Transactionalsynchronized를 적용해 동시성 제어를 시도하고 있다. 그러나 기대한 대로 작동하지 않으며, 동시성 문제를 완벽히 해결하지 못한다.

@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");
    }
}

위 코드에서 @Transactionalsynchronized를 함께 사용했지만, 아래의 이유로 동시성 문제가 발생할 수 있다.

이유 1: @Transactional의 프록시 패턴과 synchronized의 인스턴스 동기화 문제

  • @Transactional은 프록시 객체를 통해 트랜잭션 경계를 설정한다. 즉, @Transactional이 적용된 메서드는 원본 메서드를 감싸는 프록시 객체에서 호출된다.
  • synchronized 키워드는 동일 인스턴스의 메서드를 호출할 때만 동기화를 보장하지만, 트랜잭션 프록시를 통해 호출될 경우 동일 인스턴스 동기화가 제대로 이루어지지 않는다.
  • 각 쓰레드는 프록시를 통해 메서드에 접근하므로 실제 synchronized 블록의 락을 공유하지 않거나, 적절히 제어하지 못할 가능성이 생긴다. 따라서 동시성 제어가 실패할 수 있다.

이유 2: 트랜잭션의 격리 수준

  • @Transactional의 기본 격리 수준은 READ_COMMITTED이다. 이 수준에서는 각 트랜잭션이 자신이 커밋한 데이터만 읽을 수 있다. 그러나 트랜잭션이 겹쳐 실행될 경우, 다른 트랜잭션이 동시에 같은 데이터를 읽고 수정하려고 할 수 있다.
  • 예를 들어, 두 개의 트랜잭션이 addPostLike를 동시에 호출하면 중간에 변경된 데이터가 다른 트랜잭션에 영향을 주지 않거나 반영되지 않을 수 있어 동시성 문제가 발생한다.
  • 이를 해결하기 위해 트랜잭션 격리 수준을 높이거나, 비관적 락(Pessimistic Lock)을 활용해 한 트랜잭션이 완료될 때까지 다른 트랜잭션이 해당 데이터를 수정하지 못하게 하는 방법이 필요하다.

이유 3: @Transactionalsynchronized는 독립적으로 동작한다

  • @Transactional은 메서드 호출의 트랜잭션 경계만 관리하며, 동기화나 쓰레드 안전성에 대한 제어 기능이 없다.
  • 반면 synchronized쓰레드 동기화를 위해 인스턴스 내 메서드 실행을 제어하지만, 트랜잭션 격리나 커밋 타이밍에는 영향을 미치지 않는다.
  • 이로 인해 synchronized가 해당 메서드를 보호한다고 해도, 트랜잭션 내부의 작업 단위가 독립적으로 실행되기 때문에 두 메서드가 함께 동작할 때 제대로 된 동시성 제어가 이루어지지 않는다.

예시: 동시성 제어가 실패하는 시나리오

동일한 addPostLike 메서드에 여러 쓰레드가 동시에 접근한다고 가정하면 다음과 같은 일이 발생할 수 있다:

  1. Thread A가 addPostLike 메서드를 호출하고, 트랜잭션이 시작된다.
  2. Thread B가 같은 메서드를 호출하고, 다른 트랜잭션이 시작된다.
  3. synchronized가 인스턴스 동기화를 시도하지만, 프록시 객체가 다르거나 트랜잭션이 겹쳐지는 문제가 발생해 두 쓰레드가 동시에 작업을 진행한다.
  4. 결과적으로 트랜잭션 경계 내에서 두 쓰레드가 서로 다른 데이터를 읽어, 동일한 데이터에 중복 증가 또는 누락된 업데이트 문제가 발생할 수 있다.

해결 방안

@Transactionalsynchronized로 해결되지 않는 동시성 문제는 다음과 같은 방식으로 해결할 수 있다:

  1. 데이터베이스 락 사용: 비관적 락(Pessimistic Lock)이나 낙관적 락(Optimistic Lock)을 사용해 데이터베이스에서 동시성 제어를 하는 것이 더 확실하다.
  2. Redis 등 외부 락 시스템 사용: 여러 인스턴스나 쓰레드에서 동시성을 제어하려면 Redis와 같은 외부 시스템의 분산 락을 이용해 요청을 직렬화할 수 있다.

이와 같은 방안을 통해 트랜잭션과 동기화 모두 안전하게 수행하는 방식으로 동시성 문제를 해결할 수 있다.

0개의 댓글