@Transactional 어노테이션 파헤치기

smc2315·2024년 2월 27일
0

spring-boot

목록 보기
5/7
post-thumbnail

1. @Transactional이란?

1-1. Transaction

Transaction: 데이터베이스의 상태를 변환시키기 위한 더 이상 쪼갤 수 없는 최소 작업 단위

Transaction의 특징 네가지

  • 원자성 (Atomicity): 트랜잭션은 데이터베이스에 모두 반영되던가, 아니면 전혀 반영되지 않아야 한다
  • 일관성 (Consistency): 트랜잭션은 트랜잭션 전후에 데이터베이스가 일관성을 유지해야 한다
  • 독립성 (Isolation): 어떤 하나의 트랜잭션이라도, 다른 트랜잭션의 연산에 끼어들 수 없다
  • 지속성 (Durability): 트랜잭션이 성공적으로 완료됐을 경우, 결과는 영구적으로 반영되어야 한다

Transaction의 마무리 작업은 작업을 데이터베이스에 반영시키는 Commit과 작업을 취소하여 이전 상태로 되돌리는 RollBack이 존재한다.

Transaction으로 작업 단위를 쪼개서 관리를 해야 여러 작업을 하다 문제가 발생했을 경우 RollBack을 통해 이전 상태로 되돌릴 수 있다.

1-2. Spring의 Transaction

Spring에서는 Transaction을 관리하기 위한 3가지 핵심 기술을 제공한다.

  1. 트랜잭션(Transaction) 동기화
  2. 트랜잭션(Transaction) 추상화
  3. AOP를 이용한 트랜잭션(Transaction) 분리

1. 트랜잭션 동기화
트랜잭션 동기화는 트랜잭션을 시작하기 위한 Connection 객체를 특별한 저장소에 보관해두고 필요할 때 꺼내쓸 수 있도록 하는 기술이다.

트랜잭션 동기화를 통해 여러 작업을 하나의 트랜잭션으로 처리하기 위해 Connection을 매개변수로 넘기는 등 불필요한 작업이 줄어든다.

// 동기화 시작
TransactionSynchronizeManager.initSynchronization();
Connection c = DataSourceUtils.getConnection(dataSource);

// 작업 진행

// 동기화 종료
DataSourceUtils.releaseConnection(c, dataSource);
TransactionSynchronizeManager.unbindResource(dataSource);
TransactionSynchronizeManager.clearSynchronization();

위와 같이 TransactionSynchronizeManager로 트랜잭션 동기화를 진행하고 DataSourceUtils를 통해 Connection을 생성/제공 하여 트랜잭션 동기화 저장소에 바인딩하여 관리할 수 있다.

2. 트랜잭션 추상화
Spring은 JDBC,JPA, Hibernate 등 각 기술에 종속적인 코드를 이용하지 않고 트랜잭션 처리를 할 수 있도록 서비스 추상화(PSA)를 이용하여 트랜잭션 추상화 기술을 제공한다.

Spring이 제공하는 PlatformTransactionManager 인터페이스를 통해 기술에 무관한 트랜잭션 관리 코드를 작성할 수 있다.

3. AOP를 이용한 트랜잭션 분리

Spring은 AOP를 적용한 @Transactional 어노테이션을 제공하여 비즈니스 로직과 트랜잭션 경계 설정 코드를 분리할 수 있게 해준다.

AOP 관련 글은 아래의 글을 참고하자.
참고: Spring AOP 동작 원리 및 사용법

아래의 예시를 보면 비즈니스 로직에 트랜잭션 경계 설정 코드가 얽혀있어 상당히 복잡한 것을 볼 수 있다.

public void addUser(User user) {
	TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
	
	try {
		userRepository.save(user)

		this.transactionManager.commit(status);
	} catch (Exception e) {
		this.transactionManager.rollback(status);
		throw e
	}
}

이렇게 복잡하게 얽혀 있는 코드를 @Transactional을 통해 아래와 같이 깔끔하게 분리할 수 있다.

@Service
@RequiredArgsConstructor
@Transactional
public class UserService {

   private final UserRepository userRepository;

   public void addUser(User user) {
       userRepository.save(user);
   }
}

1-3. @Transactional의 세부 설정

@Transactional은 다음과 같은 설정을 지원한다.

속성타입설명
valueString사용할 트랜잭션 관리자
propagationenum: Propagation선택적 전파 설정
isolationenum: Isolation선택적 격리 수준
readOnlyboolean읽기/쓰기 vs 읽기 전용 트랜잭션
timeoutint (초)트랜잭션 타임 아웃
rollbackForThrowable 로부터 얻을 수 있는 Class 객체 배열롤백이 수행되어야 하는, 선택적인 예외 클래스의 배열
rollbackForClassNameThrowable 로부터 얻을 수 있는 Class 이름 배열롤백이 수행되어야 하는, 선택적인 예외 클래스 이름의 배열
noRollbackForThrowable 로부터 얻을 수 있는 Class 객체 배열롤백이 수행되지 않아야 하는, 선택적인 예외 클래스의 배열
noRollbackForClassNameThrowable 로부터 얻을 수 있는 Class 이름 배열롤백이 수행되지 않아야 하는, 선택적인 예외 클래스 이름의 배열

2. Transaction Propagation

2-1. Transaction Propagation이란?

트랜잭션 전파 속성(Transaction Propagation)은 이미 트랜잭션이 진행중일때 추가 트랜잭션 진행을 어떻게 진행하는지 결정하는 것이다.

전파 속성에 따라 기존의 트랜잭션에 참여할 수도 있고, 별도의 트랜잭션을 만들어 진행하는 등 여러 방법으로 전파 속성을 설정할 수 있다.

2-2. 물리 트랜잭션 & 논리 트랜잭션

물리 트랜잭션: 실제 데이터베이스에 반영되는 트랜잭션
논리 트랜잭션: Spring이 트래잭션 매니저를 통해 트랜잭션을 처리하는 단위

트랜잭션 전파 개념이 도입되면 논리 트랜잭션이 추가된다.

다음과 같이 트랜잭션 원칙을 정하며 다수의 트랜잭션을 용이하게 관리할 수 있다.

  • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됨
  • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됨

2-3. Transaction Propagation

대표적인 두 전파 속성을 살펴보자.

  1. PROPAGATION_REQUIRED
    B는 트랜잭션을 만들지 않고 A에서 진행중이 트랜잭션에 참여할 수 있다. 이 경우 B의 작업이 마무리 되고 나서, 남은 A의 작업(2)을 처리할 때 예외가 발생하면 A와 B의 작업이 모두 취소된다.

  1. PROPAGATION_REQUIRES_NEW
    B의 트랜잭션은 A의 트랜잭션과 무관하게 만든다. 이 경우 B의 트랜잭션 경계를 빠져나오는 순간 B의 트랜잭션은 독자적으로 커밋 또는 롤백되고, 이것은 A에 어떠한 영향도 주지 않는다. 즉, 이후 A가 (2)번 작업을 하면서 예외가 발생해 롤백되어도 B의 작업에는 영향을 주지 못한다.

위 두 전파 속성 외에도 여러가지 전파 속성이 존재한다.

 SUPPORTS

의미: 트랜잭션이 있으면 지원함(트랜잭션이 없어도 됨)
기존 트랜잭션 없음: 트랜잭션 없이 진행함
기존 트랜잭션이 있음: 기존 트랜잭션에 참여함

MANDATORY

의미: 트랜잭션이 의무임(트랜잭션이 반드시 필요함)
기존 트랜잭션 없음: IllegalTransactionStateException 예외 발생
기존 트랜잭션이 있음: 기존 트랜잭션에 참여함

NOT_SUPPORTED

의미: 트랜잭션을 지원하지 않음(트랜잭션 없이 진행함)
기존 트랜잭션 없음: 트랜잭션 없이 진행함
기존 트랜잭션이 있음: 기존 트랜잭션을 보류시키고 트랜잭션 없이 진행함

NEVER

의미: 트랜잭션을 사용하지 않음(기존 트랜잭션도 허용하지 않음)
기존 트랜잭션 없음: 트랜잭션 없이 진행
기존 트랜잭션이 있음: IllegalTransactionStateException 예외 발생

NESTED

의미: 중첩(자식) 트랜잭션을 생성함
기존 트랜잭션 없음: 새로운 트랜잭션을 생성함
기존 트랜잭션이 있음: 중첩 트랜잭션을 만듬

-> NESTED vs REQUIRES_NEW
NESTED는 자식 트랜잭션을 생성하여 부모 트랜잭션은 자식에게 영향을 미치지만, 자식 트랜잭션은 부모한테 영향을 미치지 않는다.

3. Transaction Isolation Level

3-1. Transaction Isolation Level이란?

트랜잭션 격리 수준(Transaction Isolation Level)은 트랜잭션에 의해 적용된 변경 사항이 서로에게 표시되는 방식이다.

격리 수준을 통해 트랜잭션에서 동시성 부작용을 방지할 수 있다.

3-2. 동시성 부작용(concurrency side effects)

동시성 부작용은 총 세 가지가 존재한다.

  1. Dirty Read: 타 트랜잭션의 변경 되지 않은 부분을 읽는다.

  2. Nonrepeatable Read: 트랜잭션 A가 데이터를 읽는 도중 트랜잭션 B가 동일한 행을 업데이트 하고 커밋하는 경우 행을 다시 읽을 때 다른 값을 가져온다.

  3. Phantom Read: 트랜잭션 A가 특정 범위의 데이터를 조회하는 도중에 트랜잭션 B가 해당 범위에 새로운 데이터를 추가하는 경우, A는 처음과 두 번째 쿼리 실행 간에 다른 결과를 받을 수 있다.

3-3. Transaction Isolation Level

트랜잭션 격리 수준은 총 4가지가 존재하고, 디폴트는 DB의 격리 수준을 따른다.

  1. Read Uncommitted

  2. Read committed

  3. Repeatable Read

  4. Serializable

JPA의 기본 격리 수준은 일반적으로 READ_COMMITTED다.

1. READ_UNCOMMITTED

  • 가장 낮은 격리 수준
  • 한 트랜잭션이 커밋되지 않은 다른 트랜잭션의 변경 내용을 읽을 수 있다.
  • Dirty read, non-repeatable read, phantom read가 모두 발생할 수 있다.
  • 데이터 일관성과 격리를 보장하지 않는다.

2. READ_COMMITTED

  • 트랜잭션이 커밋된 데이터만 읽을 수 있다.
  • Dirty read는 발생하지 않지만 non-repeatable read와 phantom read는 발생할 수 있다.
  • 다른 트랜잭션이 데이터를 수정하면 해당 트랜잭션이 커밋될 때까지 기다린다.

3. REAPETABLE_READ

  • 같은 범위의 데이터를 여러 번 읽어도 결과가 동일하다.
  • 한 트랜잭션이 읽은 데이터를 다른 트랜잭션이 수정해도 영향을 받지 않는다.
  • Non-repeatable read는 발생하지만 phantom read는 발생하지 않는다.
  • 트랜잭션이 시작될 때 읽은 데이터는 해당 트랜잭션이 종료될 때까지 유지된다.

4. SERIALIZABLE

  • 가장 높은 격리 수준
  • 트랜잭션 간에 순서대로 실행되는 것처럼 보장된다.
  • 모든 종류의 이상 현상(Dirty read, non-repeatable read, phantom read)이 발생하지 않는다.
  • 데이터 일관성과 격리를 최대한 보장한다.

3. Transactional readOnly

@Transactional 어노테이션에서 readOnly = true로 설정해 주면 읽기 전용 모드로 변경할 수 있다.

읽기 전용 모드로 설정하면 다음과 같은 이점이 있다.

  • 성능 최적화
    트랜잭션을 읽기 전용으로 설정하면 해당 메서드가 데이터를 읽기만 한다는 것을 DB에 알려줌으로써 쿼리 및 캐싱을 최적화할 수 있다.
    또한 읽기 전용으로 설정하며 데이터 변경이 일어나지 않기 때문에 변경감지를 위한 스냅샷을 저장하는 동작 또한 하지 않아 성능이 향상되는 것을 기대할 수 있다.

  • 데이터 일관성
    일반적으로 트랜잭션을 사용해서 DB에 데이터의 일관성과 무결성을 보장하기위해 사용하는데 트랜잭션을 읽기 전용으로 설정하면 실수로 데이터를 수정해서 일관성을 위반할 가능성이 낮아진다.

  • 가독성 향상
    코드를 작성하는 개발자는 @Transactional(readOnly=true)이 설정된 메서드가 DB에서 데이터를 읽기만 한다는 것을 명확하게 확인할 수 있다. 이로 인해 코드의 가독성이 향상이 된다.

참고

토비의 스프링 3.1
망나니 개발자

profile
개발일지

0개의 댓글