들어가기 앞서

트랜잭션이 중첩이 중첩으로 선언이 되면 어떻게 처리가 될까요?
이번 글에서는 Transaction 전파 속성에 대해서 알아 볼 예정입니다. 개발을 하다보면 @transactional 안에 또 @transactional을 선언 하는 경우가 많이 있습니다. 이런 경우 트랜잭션의 전파 속성에 따라 동작이 달라지게 됩니다.
트랜잭션 전파 속성이란, 트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것입니다. 트랜잭션 전파 속성에 대해 잘 모르고 계셨던 분이시라면, 한 번 이 글을 읽어봐 주세요~!
https://www.youtube.com/watch?v=b0s9RzKyHN0
물리 트랜잭션과 논리 트랜잭션
Spring 트랜잭션에서 물리적 트랜잭션(Physical Transaction)과 논리적 트랜잭션(Logical Transaction)의 개념은 서로 연관된 트랜잭션 경계를 설명할 때 사용됩니다.
1. 물리적 트랜잭션 (Physical Transaction)
물리적 트랜잭션이란 실제로 데이터베이스 수준에서 트랜잭션이 시작되고 커밋되거나 롤백되는 단위를 의미합니다.
- 실제 DB 연결(Connection)을 이용해서 begin, commit, rollback 명령을 수행합니다.
- 보통 하나의 물리적 트랜잭션은 DB 연결 세션에 하나의 트랜잭션으로 대응됩니다.
- Spring에서는 물리적 트랜잭션이 시작되는 시점은 TransactionManager가 트랜잭션을 시작한 시점입니다.
- 일반적으로 DB 연결 자원을 점유하고, DB에 실제로 명령이 전달되기 때문에 물리적이라고 표현합니다.
@Transactional
public void method() {
// 실제 데이터베이스에 begin transaction 수행
repository.save(entity);
// commit transaction 수행 또는 rollback 수행
}
2. 논리적 트랜잭션 (Logical Transaction)
논리적 트랜잭션은 애플리케이션 수준에서 트랜잭션 경계를 정의하는 개념으로, 반드시 물리적 트랜잭션과 1:1 대응되지는 않습니다.
- 여러 개의 논리적 트랜잭션이 하나의 물리적 트랜잭션 안에서 실행될 수 있습니다.
- Spring의 전파 속성(Propagation)을 통해 논리적인 트랜잭션의 범위를 결정할 수 있습니다.
- 예를 들어, 기존 트랜잭션이 있을 때 REQUIRED 옵션으로 호출된 메서드는 새로운 물리적 트랜잭션을 생성하지 않고 기존 트랜잭션에 참여하므로, 논리적 트랜잭션만 추가됩니다.
@Transactional // 논리적 트랜잭션 A (물리적 트랜잭션 시작)
public void methodA() {
methodB();
}
@Transactional // 논리적 트랜잭션 B (methodA 트랜잭션에 참여)
public void methodB() {
// 실제 물리적 트랜잭션은 이미 진행 중이므로 논리적 트랜잭션만 추가됨
}
- 위의 경우 methodA 호출 시 물리적 트랜잭션 1개, 논리적 트랜잭션은 2개입니다.
3. 물리적 트랜잭션과 논리적 트랜잭션의 관계
| 물리적 트랜잭션 | DB에서 실제로 트랜잭션을 생성하고 커밋 또는 롤백 | 처음 트랜잭션이 시작될 때 | REQUIRED (최초 트랜잭션 생성 시), REQUIRES_NEW |
| 논리적 트랜잭션 | 기존의 물리적 트랜잭션에 참여하여 트랜잭션 범위를 설정 | 기존 물리 트랜잭션 존재 시 참여 | REQUIRED, SUPPORTS, MANDATORY, NESTED |
- 물리적 트랜잭션은 DB 리소스에 직접 영향을 주는 반면, 논리적 트랜잭션은 Spring 프레임워크 내부에서 트랜잭션 경계를 관리할 때 사용하는 개념입니다.
- 모든 논리 트랜잭션이 커밋 되어야, 물리 트랜잭션이 커밋이 됩니다. 즉, 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백 됩니다.
@Transactional 전파 속성
Spring의 @Transactional에서 전파(Propagation) 속성은 트랜잭션이 시작되거나 진행될 때 기존 트랜잭션과 어떻게 동작할지를 결정하는 설정입니다.
@Transactional(propagation = Propagation.XXX) 형태로 설정하며, 대표적인 전파 속성은 다음과 같습니다.
1. REQUIRED (기본값)
- 기존 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜잭션을 생성합니다.
- 대부분의 상황에서 사용하는 기본적인 옵션입니다.
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// 기존 트랜잭션(methodA)에 참여
}
2. REQUIRES_NEW
- 항상 새로운 트랜잭션을 생성합니다.
- 이미 진행 중인 트랜잭션은 잠시 보류되고, 새로 생성된 트랜잭션이 완료될 때까지 기다립니다.
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// methodA의 트랜잭션과 별도의 새 트랜잭션으로 동작
}
- methodB에서 예외가 발생하면 methodB의 트랜잭션만 롤백됩니다. methodA는 영향을 받지 않을 수 있습니다.
3. SUPPORTS
- 이미 진행 중인 트랜잭션이 있다면 참여합니다.
- 진행 중인 트랜잭션이 없다면, 비트랜잭션(non-transactional)으로 실행됩니다.
@Transactional(propagation = Propagation.SUPPORTS)
public void method() {
// 트랜잭션이 있으면 참여하고, 없으면 비트랜잭션으로 실행
}
4. NOT_SUPPORTED
- 이미 진행 중인 트랜잭션을 보류하고 비트랜잭션 상태로 실행합니다.
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void method() {
// 기존 트랜잭션을 보류하고, 트랜잭션 없이 실행
}
5. NEVER
- 진행 중인 트랜잭션이 없어야만 실행 가능합니다.
- 기존 트랜잭션이 있으면 예외가 발생합니다.
@Transactional(propagation = Propagation.NEVER)
public void method() {
// 트랜잭션이 있으면 예외 발생
}
6. MANDATORY
- 기존 트랜잭션이 반드시 존재해야 합니다.
- 트랜잭션이 없으면 예외가 발생합니다.
@Transactional(propagation = Propagation.MANDATORY)
public void method() {
// 반드시 트랜잭션 안에서만 호출되어야 함
}
7. NESTED
- 기존 트랜잭션이 있으면 중첩(Nested) 트랜잭션을 실행합니다.
- 중첩된 트랜잭션에서 롤백되면, 부모 트랜잭션까지 롤백되지 않고 중첩된 범위만 롤백됩니다.
- 기존 트랜잭션이 없으면 REQUIRED처럼 동작하여 새 트랜잭션을 생성합니다.
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
// methodB만 별도의 중첩된 트랜잭션으로 실행됨
// methodB에서 롤백 시 methodA는 전체 롤백되지 않고 영향을 받지 않음
}
간단한 정리
| REQUIRED | 참여 | 새로 생성 |
| REQUIRES_NEW | 기존 보류하고 새로 생성 | 새로 생성 |
| SUPPORTS | 참여 | 트랜잭션 없이 실행 |
| NOT_SUPPORTED | 기존 보류, 트랜잭션 없이 실행 | 트랜잭션 없이 실행 |
| NEVER | 예외 발생 | 트랜잭션 없이 실행 |
| MANDATORY | 참여 | 예외 발생 |
| NESTED | 중첩된(Nested) 트랜잭션으로 참여 | 새로 생성(REQUIRED와 동일) |
실제 개발에서는 보통 REQUIRED가 기본이지만, 특정 로직에서 트랜잭션 경계를 명확히 관리하거나, 예외 상황에서 트랜잭션의 롤백 범위를 세밀하게 제어하고 싶을 때 위의 옵션을 사용하여 설정할 수 있습니다.
비즈니스 요구사항
실제 비즈니스 요구사항에서 동작을 알아 보겠습니다.

영화를 예매하는 로직에서 로그를 저장하는 서비스에서 장애가 발생하면 어떻게 될까요? 일반적인 상황에서는 propagation이 required라서 전체 롤백이 진행 됩니다.
하지만 로그 저장에 실패 해서 예매가 불가능해 진다면, 많은 사람들이 서비스에 실망을 할 수도 있습니다. 로그가 실패 하더라도 영화 예매에는 영향이 없도록 하고 싶으면 어떻게 해야 될까요?

propagation을 requires_new로 설정한다면, 기존 트랜잭션이 존재할 경우, 새로운 물리 트랜잭션을 생성합니다. 따라서 로그 저장시 장애가 발생하더라도 영화 예매 서비스에는 영향을 주지 않습니다.
하지만 이렇게 물리 트랜잭션을 생성하는 것에는 신경을 많이 써야 합니다. 물리 트랜잭션의 수 만큼 DB 커넥션이 생성 되기 때문에 많은 부하를 받게 되며, DB 커넥션을 위한 성능 저하도 발생하기 때문입니다.
'테크톡 딥다이브' 카테고리의 다른 글
| DBCP (Database Connection Pool) (0) | 2025.04.15 |
|---|---|
| 데이터베이스 락 (Lock) with MySQL (0) | 2025.03.31 |
| 네이버의 Fixture Monkey 사용기 (5) | 2025.02.24 |
| 의미 있는 테스트 코드에 대해 알아보자 (0) | 2025.02.24 |
| 카카오 선물하기 팀의 캐싱 전략 (하이브리드 캐시와 캐시 웜업 자동화) (1) | 2025.01.07 |