트랜잭션과 @Transactional
Spring

트랜잭션과 @Transactional

이 글은 망나니 개발자님의 [Spring] 트랜잭션에 대한 이해와 Spring이 제공하는 Transaction 핵심 기술을 읽고 정리한 글입니다.

트랜잭션이란?

트랜잭션은 더 이상 쪼갤 수 없는 작업의 최소 단위를 의미한다. 즉, 여러 작업을 진행하다가 문제가 생겼을 경우 롤백하기 위해 트랜잭션을 단위로서 사용할 수 있다. 트랜잭션에는 commit, rollback 두 가지의 경우가 존재한다. 모두 성공하여 commit 되거나, 하나라도 실패하면 rollback 되는 것이다. 

 

Spring에서 제공하는 Transaction 기능

Spring은 트랜잭션과 관련된 핵심 기술을 3가지 제공한다.

1. 트랜잭션 동기화

2. 트랜잭션 추상화

3. 트랜잭션 분리

 

1. 트랜잭션 동기화

개발자들이 JDBC의 모든 커넥션을 하나의 트랜잭션으로 관리하기 위한 작업을 직접 수행한다면, 매우 번거로울 것이다. Spring에서는 이러한 문제를 해결하고자 트랜잭션 동기화 기술을 지원한다. 즉, 트랜잭션을 시작하기 위한 자원을 특별한 저장소에 보관해두고 꺼내쓸 수 있도록 한다. 

 

하지만 이 기술은 JDBC에 종속적이기 때문에 이를 해결하기 위해 트랜잭션 추상화가 등장했다.

 

2. 트랜잭션 추상화

애플리케이션에서 사용할 수 있는 기술마다 종속적인 코드를 이용하지 않고도 일관되게 트랜잭션을 처리할 수 있도록 트랜잭션 추상화를 지원한다. 

이미지 출처: https://mangkyu.tistory.com/154

Spring이 제공하는 트랜잭션 경계 설정을 위한 추상 인터페이스는 PlatformTransactionManager 이다. 이를 통해 사용하는 기술과 무관하게 트랜잭션을 공유/ 커밋/ 롤백할 수 있게 되었다.

 

하지만 비즈니스 코드와 트랜잭션 코드가 결합되는 것은 피하지 못했다.

 

public void updateMember(Member member) {
    TransactionStatus status 
        = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
    
    try {
        if (validateMember(member)) {
            memberRepository.save(member);
        }
        this.transactionManager.commit(status);
    } catch (Exception e) {
        this.transactionManager.rollback(status);
        throw e;
    }
}

 

위 코드처럼 비즈니스 코드와 트랜잭션 코드가 결합되어 메서드 내에서 2가지 책임을 갖고 있게 되었다. 따라서 Spring에서는 트랜잭션 부분을 핵심 비즈니스 로직과 분리하였다.

 

3. 트랜잭션 분리

트랜잭션과 핵심 비즈니스 로직을 분리하기 위해 선언적 트랜잭션 @Transactional 을 지원하게 되었다.

 

선언적 트랜잭션은 여러 트랜잭션 적용 범위를 묶어 하나의 커다란 트랜잭션 경계를 만들 수 있다. 우리는 Spring이 트랜잭션을 어떻게 진행시킬 지 결정하도록 전파 속성을 전달해야한다.

 

트랜잭션 전파 속성

전파 속성 설명
Required(default) 모든 트랜잭션 매니저가 지원한다. 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다.
Supports 이미 시작된 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 진행한다.
Mandatory 이미 시작된 트랜잭션이 있으면 참여하고 없으면 예외를 뱉는다.
Requires_new 항상 새로운 트랜잭션을 시작한다. 이미 시작된 트랜잭션이 있으면 트랜잭션을 잠시 보류한다.
Not_supported 이미 진행중인 트랜잭션이 있으면 이를 보류하고 트랜잭션을 사용하지 않도록 한다.
Never 이미 진행중인 트랜잭션이 있으면 예외를 발생시키며 트랜잭션을 사용하지 않도록 강제한다.
Nested 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. 중첩 트랜잭션이란 트랜잭션 안에 트랜잭션을 만드는 것으로, 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랜잭션에게 영향을 주지 않는다.

 

트랜잭션 격리수준은 동시에 여러 트랜잭션이 진행될 때 트랜잭션의 작업 결과를 다른 트랜잭션에 어떻게 노출할 것인지를 결정한다.

트랜잭션 격리 수준

격리 수준 설명
Default 데이터 액세스 기술, DB 드라이버의 디폴트 설정을 따른다.
Read_uncommited 가장 낮은 격리 수준으로서 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 노출된다.
Read_commited 가장 많이 사용된다. Spring에서의 Default이다.
다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없되, 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있다.
Repeatable_read 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 없도록 막아준다. 하지만 새로운 로우를 추가하는 것은 막지 않는다.
Serializable 가장 강력한 격리 수준으로, 트랜잭션을 순차적으로 진행한다. 

 

읽기 전용

  • 읽기 전용 (readOnly=true)으로 설정함으로써 성능을 최적화한다.
  • 쓰기 작업이 일어나는 것을 의도적으로 방지한다.

읽기 전용으로 설정된 트랜잭션 작업인 경우 일반적으로 읽기전용 트랜잭션이 시작된 이후 CUD 작업이 발생하면 예외를 발생시킨다.

 

롤백/커밋 예외

선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백하고, 예외가 발생하지 않았거나 체크 예외가 발생하였다면 커밋한다. 

rollbackFor를 활영하여 롤백/커밋의 동작 방식을 변경할 수 있다.

 

시간 제한

timeout 속성을 이용해 트랜잭션에 제한 시간을 지정할 수 있다.