트랜잭션 전파는 내부 작업을 기존 트랜잭션과 함께 처리할지 분리할지 결정하고, 격리 수준은 동시에 실행되는 트랙잭션끼리 데이터를 어디까지 볼 수 있는지 결정한다.
@Transactional 메서드 안에서 다른 @Transactional 메서드를 호출하면, 두 작업을 같은 트랜잭션으로 묶을지 별도로 실행할지 결정해야 한다. 이를 트랜잭션 전파라고 한다.
| 옵션 | 동작 | 주요 용도 |
|---|---|---|
REQUIRED | 기존 트랜잭션이 있으면 참여하고, 없으면 새로 생성 | 일반적인 비즈니스 로직 |
REQUIRES_NEW | 기존 트랜잭션을 중단하고 별도의 트랜잭션 생성 | 본 작업이 실패해도 남겨야 하는 로그·이력 |
REQUIRED는 기본값이므로 대부분 별도로 작성하지 않는다. REQUIRES_NEW는 메인 기능의 성공 여부와 관계없이 반드시 기록할 데이터에 사용한다.

REQUIRED와 REQUIRED_NEW의 트랜잭션 참여 방식
기존 트랜잭션에 참여하는 REQUIRED와 별도의 물리적 트랜잭션을 생성하는 REQUIRES_NEW의 차이를 비교한 그림이다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final OrderLogService orderLogService;
private final PointService poinService;
@Transactional // 트랜잭션 A
public void order(OrderRequest request) {
orderLogService.saveLog(request);
orderRepository.save(new Order(request));
pointService.usePoint(request); // 예외 발생
}
}
@Service
@RequiredArgsContructor
public class OrderLogService {
private final OrderLogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(OrderRequest request) {
logRepository.save(new OrderLog(request));
}
}
실행 흐름은 다음과 같다.
트랜잭션 A 시작
↓
saveLog() 호출
↓
A 일시 중단
↓
트랜잭션 B 시작
↓
주문 시도 이력 저장
↓
B Commit
↓
A 재개
↓
포인트 차감 중 예외
↓
A Rollback
결과는 다음과 같다.
주문 데이터 → Rollback으로 취소
주문 시도 이력 → 별도 Commit되어 유지
REQUIRED가 한 배를 타는 관계라면, REQUIRES_NEW는 서로 다른 배를 타는 관계다. 부모 트랜잭션이 롤백되어도 이미 커밋된 별도 트랜잭션은 영향을 받지 않는다.
첫째, 로그 저장 중 발생한 예외가 외부로 전달되면 주문 트랜잭션까지 실패할 수 있다. 로그 실패가 본 기능을 중단시키면 안 되는 정책이라면 예외를 별도로 처리해야 한다.
try {
orderLogService.saveLog(request);
} catch (RuntimeException e) {
// 로그 실패 처리
}
둘째, 같은 클래스 내부에서 REQUIRES_NEW 메서드를 직접 호출하면 Spring 프록시를 거치지 않아 새 트랜잭션이 적용되지 않을 수 있다. 전파 옵션이 필요한 기능은 별도 Service로 분리하는 편이 안전하다.
여러 트랜잭션이 동시에 같은 데이터를 조회하고 수정하면 결과가 달라질 수 있다. 격리 수준은 한 트랜잭션이 다른 트랜잭션의 변경 내용을 어디까지 볼 수 있는지 결정한다.
격리 약함 격리 강함
동시 처리 성능 높음 데이터 일관성 높음
READ READ REPEATABLE SERIALIZABLE
UNCOMMITTED COMMITTED READ
───────────────────────────────────────────────►
| 현상 | 의미 | 주요 원인 |
|---|---|---|
| Dirty Read | 다른 트랜잭션이 아직 커밋하지 않은 값을 읽음 | 미커밋 데이터 노출 |
| Non-Repeatable Read | 같은 행을 다시 읽었는데 값이 달라짐 | 기존 행의 UPDATE |
| Phantom Read | 같은 범위를 다시 조회했는데 행 개수가 달라짐 | INSERT 또는 DELETE |
Non-Repeatable Read = 기존 행의 값이 달라지는 문제
Phantom Read = 조회 결과의 행 개수가 달라지는 문제
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | 발생 가능 | 발생 가능 | 발생 가능 |
| READ COMMITTED | 방지 | 발생 가능 | 발생 가능 |
| REPEATABLE READ | 방지 | 방지 | 발생 가능 |
| SERIALIZABLE | 방지 | 방지 | 방지 |
MySQL InnoDB의 기본 격리 수준은 REPEATABLE READ이며, 일반적인 Spring 설정에서는 별도 변경 없이 DB 기본값을 따른다.
일반적인 비즈니스 로직은 다음 설정으로 충분하다.
@Transactional
public void process() {
// 비즈니스 로직
}
이는 다음 기본 설정을 사용한다.
Propagation = REQUIRED
Isolation = DEFAULT
전파와 격리 수준은 특별한 이유 없이 변경하지 않는다.
일반 비즈니스 로직
→ REQUIRED
본 기능이 실패해도 남아야 하는 이력
→ REQUIRES_NEW 검토
격리 수준
→ DB 기본값 우선
격리 수준을 높이면 이상 현상은 줄어들지만 Lock과 대기가 증가해 동시 처리 성능이 낮아질 수 있다. 따라서 문제가 확인되지 않은 상태에서 무조건 높은 수준을 선택하는 것은 적절하지 않다.
트랜잭션 전파와 격리 수준은 해결하는 문제가 다르다.
전파
→ 여러 메서드의 작업을 같은 트랜잭션으로 묶을지 결정
격리 수준
→ 동시에 실행되는 트랜잭션이 서로의 데이터를 어디까지 볼지 결정
핵심은 REQUIRED, REQUIRES_NEW, 세 가지 이상 현상을 구분하는 것이다. 일반적인 상황에서는 기본값을 사용하고, 본 기능과 운명을 같이하면 안 되는 기록이 있을 때만 REQUIRES_NEW를 검토한다.