본문 바로가기

programming/java, spring

트랜잭션 격리수준과 JPA 낙관적 락을 통한 동시성 제어

트랜잭션 격리수준(Transaction Isolation Level)

트랜잭션 격리수준은 동시에 실행되는 트랜잭션들 사이에서 어느 정도의 격리를 유지할지를 결정하는 것이다. 격리수준이 높을수록 동시성 문제가 발생할 가능성이 줄어든다.

트랜잭션 격리수준은 크게 네 가지로 구분할 수 있다.

 

1.READ UNCOMMITTED

  • 가장 낮은 격리 수준
  • 트랜잭션에서 수정 중인 데이터에 대해서도 다른 트랜잭션에서 읽기가 가능함
  • 데이터의 일관성이 보장되지 않음

2.READ COMMITTED

  • 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음
  • 트랜잭션에서 수정 중인 데이터는 다른 트랜잭션에서 조회 불가
  • Dirty Read 문제를 해결할 수 있음
  • UNREPETABLE READ 발생

3.REPEATABLE READ

  • 한 트랜잭션 내에서 같은 쿼리를 여러 번 실행해도 결과가 항상 같음
  • 다른 트랜잭션에서 데이터를 수정해도 읽기 작업은 변경된 데이터를 볼 수 없음
  • Phantom Read 문제가 발생할 수 있음.

4.SERIALIZABLE

  • 가장 높은 격리 수준
  • 모든 데이터에 대해 락을 걸어 다른 트랜잭션이 접근할 수 없도록 함
  • 모든 문제를 해결할 있지만, 성능상 이슈가 있을 있음

각각의 격리수준은 일관성과 동시성 사이에서 트레이드 오프 관계를 갖는다. 따라서, 상황에 맞게 적절한 격리수준을 선택해야 한다.

 

낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)

JPA에서는 데이터베이스 동시성을 제어하기 위해 락 기능을 지원한다.

 

낙관적 락

  • Optimistic Locking은 락을 걸지 않고, 데이터 충돌이 일어날 가능성이 낮은 것으로 가정하고 트랜잭션을 실행하는 방식
  • 데이터를 읽어와서 트랜잭션을 시작할 때 버전 정보(version)를 함께 가져옴
  • 트랜잭션이 종료되기 전에 다시 버전 정보를 체크하여, 다른 트랜잭션이 해당 데이터를 수정하지 않았다는 것을 확인하고 커밋
  • 만약 다른 트랜잭션이 해당 데이터를 수정한 경우, 충돌이 발생하고 예외가 발생
  • Optimistic Locking 락을 걸지 않기 때문에 다른 트랜잭션이 데이터를 변경하지 않을 가능성이 높은 경우에 유용

 

비관적 락

  • Pessimistic Locking은 데이터에 락을 걸어서 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 막는 방식
  • 데이터를 읽을 때 락을 걸고, 해당 락이 해제될 때까지 다른 트랜잭션에서 해당 데이터를 변경하지 못하도록 함
  • Pessimistic Locking은 SERIALIZABLE 격리 수준과 유사한 효과를 가져오지만, 락을 걸기 때문에 성능이 떨어질 수 있음
  • Pessimistic Locking 특히 데이터를 변경하는 트랜잭션이 길고, 락을 걸어야 데이터가 많은 경우에 적합

 

현재 내 애플리케이션에서 동시성 이슈가 발생할 수 있는지 생각해보기로 했다.

 

엔티티 erd

 

게시글 하나에 여러 training이 묶여 있고, training은 enum 필드를 가지고 있다. body part는 관리자가 변경하는 값이다.

만약 사용자가 게시글을 수정하는 도중에, body part 값이 변경된다면? 사용자가 수정하고 있는 게시글이 변경된 body part 값을 참조하여 수정이 이루어질 가능성이 있다. 즉,  데이터 정합성 문제가 발생할 가능성이 있다. 이는 사용자가 수정 중인 게시글과 변경된 body part 값이 일치하지 않아서 생기는 문제다.

 

이러한 상황을 방지하기 위해서는, 다음과 같은 방법들을 고려할 수 있다.

 

  • 관리자가 body part값을 변경할 때, 수정중인 게시글이 해당 body part 값을 참조하지 못하도록 한다.
  • body part 값이 수정되는 경우, 해당 값을 참조하는 모든 training의 수정을 막는다.
  • 사용자가 게시글 수정을 완료할 때, 변경된 body part 값을 다시 확인한다.
  • 게시글을 수정할 때, 락 기능을 추가해 동시에 수정하지 못하도록 한다.

 

JPA 낙관적 락 기능을 사용하기로 결정하고, 어플리케이션에 적용했다.

 

먼저 Article 엔티티 클래스에서는 @Version 어노테이션을 사용하여 낙관적 락을 적용했다. 이 어노테이션을 사용하면 JPA가 엔티티를 조회할 때 버전 정보를 함께 조회하고, 엔티티를 수정할 때는 이 버전 정보를 체크하여 동시 수정이 일어나지 않도록 한다.

 

@Getter
@Setter
@Entity
public class Article extends BaseTimeEntity {

    // 생략

    @Version
    private Long version;

    // 생략

}

 

다음으로 ArticleRepository 레파지토리에서는 @Lock 어노테이션을 사용하여 낙관적 락을 적용했다. 이 어노테이션을 사용하면 JPA가 엔티티를 조회할 때 낙관적 락을 적용한다.

 

@RepositoryRestResource
public interface ArticleRepository extends JpaRepository<Article, Long> {

    @Lock(LockModeType.OPTIMISTIC)
    Article getReferenceById(Long id);

    // 생략

}

 

마지막으로 ArticleService 서비스 클래스에서는 ObjectOptimisticLockingFailureException 예외가 발생하면 ArticleConcurrencyException 예외로 변환하여 처리한다. 이 예외는 다른 사용자가 이미 게시글을 수정하여 버전 정보가 변경되었을 때 발생한다.

 

@Service
@Slf4j
public class ArticleService {

    // 생략

    public void updateArticle(long articleId, ArticleDto dto) {
        try {
            Article article = articleRepository.getReferenceById(articleId);
            article.update(dto);
        } catch (NullPointerException e) {
            throw new InvalidArticleException(e.getMessage());
        } catch (ObjectOptimisticLockingFailureException e) {
            throw new ArticleConcurrencyException(e.getMessage());
        }
    }

    // 생략

}

 

이렇게 구현하면 동시에 여러 사용자가 게시글을 수정할 때 발생할 수 있는 데이터 정합성 문제를 방지할 수 있다.