영호

[Spring] @Transactional 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이 본문

Spring

[Spring] @Transactional 물리 트랜잭션과 논리 트랜잭션 커밋 동작 차이

0h0 2023. 10. 14. 17:28

들어가면서

이번 글에서는 @Transactional 이 커밋되는 과정을 물리 트랜잭션, 논리 트랜잭션 2 경우에 대해 디버깅을 통해 공부한 과정을 정리할 예정이다.

스프링 트랜잭션에는 물리 트랜잭션과 논리 트랜잭션이란 개념이 있다.

물리 트랜잭션은 데이터베이스 커넥션을 통해 우리의 쿼리가 실제 커넥션을 통해 커밋/롤백 하는 역할을 한다.

논리 트랜잭션은 A라는 트랜잭션에 B 라는 트랜잭션이 참가하는 경우. 즉, 하나의 트랜잭션 내부에 다른 트랜잭션이 추가로 사용하는 경우 이 트랜잭션들을 논리 트랜잭션 이라고 한다.
트랜잭션이 하나라면 물리, 논리 트랜잭션을 구분하지 않는다.

트랜잭션의 COMMIT 동작 흐름

@TransactIonal 을 활용한 트랜잭션은 AbstractPlatformTransactionManager 클래스의 commit() 을 통해 이루어진다. 그런데 해당 메서드의 내부를 보면 아래와 같다.

여기서 status 는 트랜잭션의 상태다. 내부에 다양한 값이 들어있지만, 그 중 boolean newTransaction 필드가 존재한다. 이를 통해, 커밋하려는 트랜잭션이 가장 외부에서 생성된 물리 트랜잭션인지 논리 트랜잭션인지 판단하는 것으로 추측한다.

만약 현재 트랜잭션이 기존 트랜잭션에 참가한 논리 트랜잭션이라면 newTransaction = false 로 설정되어 있어 COMMIT 되지 않는다.

이를 실제 예제 코드를 통해 살펴보자.

@Service
@RequiredArgsConstructor
@Transactional
public class Publisher {

    private final ApplicationEventPublisher publisher;
    private final EventEntityRepo eventEntityRepo;

    public void publish() {
        eventEntityRepo.save(new EventEntity());
        publisher.publishEvent(new MyEvent());
    }
}

이 코드는 아래와 같은 순서로 동작한다.

  1. Publisher.publish() 메서드 호출로 인해 트랜잭션을 얻고 실행한다.
  2. eventEntityRepo.save() 를 호출하면서 동일하게 트랜잭션을 얻고 실행한다.
    • eventEntityRepo.save() 작업은 중간에 참여하는 다른 트랜잭션이 없기 때문에 바로 COMMIT 까지 진행한다.
  3. publish() 메서드가 끝나면서 해당 메서드를 시작하면서 얻은 1번 트랜잭션을 COMMIT 한다.

위 과정을 살펴보기 전에 한 가지 명심하고 가면 좋은 점은 물리 트랜잭션은 트랜잭션의 status 중 newTransaction = true 이고, 논리 트랜잭션은 newTransaction = false 라는 것이다.

1번. Publisher.publish() 호출을 통한 트랜잭션 생성

아래 코드는 AbstractPlatformTransactionManager 의 getTransaction() 코드다.

브레이크 포인트(110 줄)로 설정한 부분은 추상 클래스인 AbstractPlatformTransactionManager.doGetTransaction() 을 오버라이딩한 자식 클래스의 메서드가 호출된다. JPA 를 사용하면 JpaTransactionManager.doGetTransaction() 이 호출된다.

110번 라인(브레이크 포인트) 까지 실행하고 만들어진 transation 인스턴스를 이용해 132번 라인에서 startTransaction() 을 호출해 TransactionStatus 인스턴스를 생성한다.

여기서 눈여겨 볼 점은 newTransaction = true 로 설정해주는 것이다.

이후 TransactionSynchronizationManager 의 스레드 로컬에 디비 작업 관련 리소스를 바인딩 해주고 autocommit = false 로 설정하는 작업 등을 수행함으로써 트랜잭션 커밋 직전까지의 작업을 수행하면서 1번 작업이 종료된다.

2번 eventEntityRepo.save() 를 호출을 통한 트랜잭션 생성 및 커밋

아직 Publisher.publish() 메서드가 끝나지 않았기 때문에 1번 트랜잭션의 COMMIT 이 발생하지 않고 2번 작업으로 넘어간다.

eventEntityRepo.save() 트랜잭션 생성 과정

SimpleJpaRepository 의 메서드에도 @Transactional 이 붙어 있기 때문에 2번 작업을 수행하기 위해서 트랜잭션을 시작하는 작업이 필요하다.

이때 1번 과는 살짝 다른 과정이 발생하는데 이를 디버깅을 통해 살펴보자.

위 상황을 살펴보면 1번과 동일하게 AbstractPlatformTransactionManager.getTransaction() 이 호출됐지만 publish() 에서 얻은 트랜잭션이 존재하기 때문에 if 문에 걸리게 된다.

this.isExistingTransaction() 의 동작 방식은 JpaObject transaction의 entityManagerHolder != null && entityManagerHolder.entityManagerHolder.isTransactionActive() 을 통해 결정된다.

110 번 줄이 실행되면서 JpaTransactionManager 내부적으로 TransactionSynchronizationManager 의 스레드 로컬을 통해 값이 있으면 transaction 에 entityManager 값을 할당해준다. 이미 publish() 의 트랜잭션 할당 과정에서 스레드 로컬에 디비 관련 리소스가 할당됐기 때문에 2번 트랜잭션을 만들면서 entityManager 가 null 이 아닌 스레드로컬에 할당된 값이 바인딩 되면서 해당 if 문에 걸리게 되는 것이다.

그래서 이번에는 113 번 줄(this.handleExistingTransaction())을 통해 TransactionStatus 인스턴스가 생성된다.

this.handleExistingTransaction() 코드는 매우 길기 때문에 실제 수행되는 코드만 캡쳐했다. else 로직이 수행된다.

이를 통해 newTransaction = false 인 트랜잭션이 생성됐다.

트랜잭션 커밋 과정

이제 eventEntityRepo.save() 메서드가 종료되면서 해당 트랜잭션은 커밋 되어야 한다.

이 과정에서 AbstractPlatformTransactionManager.processCommit() 이 호출된다. 이 코드도 매우 길기 때문에 호출되는 부분만 캡쳐했다.

isNewTransaction() == false 로 인해 doCommit() 메서드가 실행되지 않기 때문에 해당 트랜잭션은 commit 되지 않는다.

실제로 sql 로그를 확인해보면 이 시점에는 insert 쿼리가 나가지 않는 것을 확인할 수 있다.

3번 Publisher.publish() 트랜잭션 커밋

가장 외부에서 트랜잭션을 얻은 Publisher.publish() 메서드가 종료되는 시점에 아래의 코드가 실행된다. 2번과 다르게 newTransaction == true 이기 때문에 this.doCommit() 이 호출된다

doCommit()은 JpaTransactionManager 가 오버라이딩한 doCommit()이 실행된다.

코드를 보면 해당 트랜잭션이 가지고 있는 entityManager 를 가지고 와서 commit() 을 호출한다. 이런 식으로 계속 타고 들어가다 보면

flush() 를 호출해서 entityManager 가 저장하고 있는 쿼리가 실질적으로 외부 트랜잭션의 커넥션을 이용해 데이터베이스에 커밋된다.

그럼 물리 트랜잭션과 논리 트랜잭션은 entityManager 를 공유할까?

커밋 과정을 다시 한 번 살펴보면 물리 트랜잭션 한 번의 커밋으로 논리 트랜잭션에서 발생한 쿼리도 같이 flush() 가 된다.

이 부분에서 아래와 같은 의문점이 생겼다.

중간에 참여한 트랜잭션에서 발생한 쿼리는 어디서 관리되길래 외부 트랜잭션 커밋 한 번으로 그동안 발생한 쿼리가 데이터베이스로 나가는걸까?

그래서 물리 트랜잭션과 논리 트랜잭션의 entityManager 를 비교해보기 위해 다시 한 번 디버깅을 했다.

그 결과 물리 트랜잭션이 가지고 있는 entityManager 를 해당 물리 트랜잭션에 참여한 논리 트랜잭션이 그대로 사용하고 있는 것을 확인 할 수 있었다.

Comments