@Transactional(propagaion = Propagation.전파타입) 형태로 사용함| 타입 | 실행 중인 트랜잭션 있음 | 실행 중인 트랜잭션 없음 |
|---|---|---|
| REQUIRED | 기존 트랜잭션 참여 | 새 트랜잭션 생성 |
| MANDATORY | 기존 트랜잭션 참여 | 예외 발생 |
| REQUIRED_NEW | 기존 트랜잭션 보류, 새 트랜잭션 생성 | 새 트랜잭션 생성 |
| SUPPORTS | 기존 트랜잭션 참여 | 트랜잭션 없이 진행 |
| NOT_SUPPORTED | 기존 트랜잭션 참여 | 트랜잭션 없이 진행 |
| NEVER | 예외 발생 | 트랜잭션 없이 진행 |
| NESTED | 중첩 트랜잭션 생성 | 새 트랜잭션 생성 |



논리 트랜잭션도 커밋과 롤백을 요청할 수 있지만 실제 데이터베이스에 적용되진 않음
모든 논리 트랜잭션이 커밋돼야 물리 트랜잭션이 커밋된다.
신규 트랜잭션만이 물리 트랜잭션을 종료(커밋, 롤백)할 수 있다.

REQUIRED1. (신규 생성) Creating new transaction ... : PROPAGATING_REQURED, ...
2. 영화 예매 트랜잭션 시작
3. Participatin in existing transaction (영화 예매 Repo에 트랜잭션 전파 속성 지정 안함 -> 기본값인 REQUIRED라 기존 부모 트랜잭션에 참가)
4. 로그 저장 트랜잭션 시작
5. Participating in existing transaction (로그 저장 Repo에 트랜잭션 전파 속성 지정 안함 -> 기본값인 REQUIRED라 기존 부모 트랜잭션에 참가)
6. 영화 예매 트랜잭션 커밋
7.Completing transation for ... Initiating transaction commit
@Transactional 옵션 지정 안해서 전파 속성 기본인 Required 지정...(생략)
1. RuntimeException: 로그 저장 실패
2. 로그 저장 실패 -> 신규 트랜잭션이 아니기 때문에 물리 롤백 진행 불가 -> Rollback Only 지정만
3. 발생한 예외는 영화 Service로 넘어감 (호출부)
4. 영화 Service에서 따로 예외 처리를 해주지 않으면 Transaction Manager에게 롤백 요청 -> 영화 서비스는 신규 트랜잭션이기 때문에 물리 Rollback
rollback-only 속성 적어두기만 함
(생략)
1. 로그 저장 트랜잭션 시작
2. Suspending current transaction, creating new transaction ..
(기존 트랜잭션 보류하고 신규 트랜잭션 생성)
3. RuntimeException: 로그 저장 실패
-> 신규 트랜잭션이기 때문에 물리 롤백
4. 던저진 예외는 서비스로감 -> 예외를 적절히 처리 -> 물리 커밋
논리 트랜잭션이 하나라도 롤백되면 관련된 물리 트랜잭션은 롤백된다.
해결 : REQUIRES_NEW 전파 타입을 사용해 트랜잭션을 분리해야한다.
주의 : 트랜잭션 수 만큼 DB 커넥션이 생성된다.
원하는 의도대로 자식이 롤백되도 부모에서 영향 없도록 하려면 4번만 가능하다
| 케이스 | 전파 타입 | 자식 예외 | 부모 catch | 자식 결과 | 부모 결과 |
|---|---|---|---|---|---|
| 1 | REQUIRED | throw | ❌ | 롤백 | 롤백 |
| 2 | REQUIRED | throw | ✅ | 롤백 | 롤백 (UnexpectedRollbackException) |
| 3 | REQUIRES_NEW | throw | ❌ | 롤백 | 롤백 (예외 전파) |
| 4 | REQUIRES_NEW | throw | ✅ | 롤백 | 커밋 ✅ |
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/movie")
@Tag(name = "영화예매", description = "영화예매 API")
public class MovieController {
private final MovieService movieService;
@PostMapping("/reserve")
@Operation(summary = "영화예매", description = "영화예매 + 로그 저장")
public String reserve() {
movieService.reserveWithLog();
return "OK";
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class MovieService {
private final MovieReservationService reservationService;
private final ReservationLogService logService;
@Transactional // 여기서 물리 트랜잭션 시작
public void reserveWithLog() {
reservationService.reserve("짱구는 못말려 극장판", "테스터유저");
try {
logService.saveLog("철현 예약 성공 로그 기록 시도");
} catch (Exception e) {
log.info("논리 트랜잭션 예외 catch 처리 완료 과연 커밋이 될까?");
System.out.println("outer에서 로그 예외 캐치: " + e.getMessage());
}
log.info("=== 영화 예매 완료 ===");
// 여기까지는 정상적으로 끝난 것처럼 보이지만...
// 메서드가 리턴되는 시점에 커밋을 시도하다가
// 이미 rollback-only 상태라 UnexpectedRollbackException 터짐
}
}
-------
@Service
@RequiredArgsConstructor
public class MovieReservationService {
private final MovieReservationRepository reservationRepository;
@Transactional // 기본: PROPAGATION_REQUIRED
public void reserve(String movieTitle, String username) {
MovieReservation reservation = new MovieReservation();
reservation.setMovieTitle(movieTitle);
reservation.setUsername(username);
reservationRepository.save(reservation);
}
}
----------
@Service
@RequiredArgsConstructor
@Slf4j
public class ReservationLogService {
private final ReservationLogRepository logRepository;
@Transactional // 기본: PROPAGATION_REQUIRED (outer 트랜잭션에 참여)
public void saveLog(String message) {
ReservationLog reservationLog = new ReservationLog();
reservationLog.setMessage(message);
logRepository.save(reservationLog);
// 테스트용으로 일부러 예외 발생
if (true) {
log.info("[영화 예매 로그 저장]논리 트랜잭션에서 예외 발생, rollback-only 표기");
throw new RuntimeException("로그 저장 중 에러!");
}
}
}
------
rollback-only 표기를 수정되지 않아 UnexpectedRollbackException 가 발생한다.2025-11-22T15:52:06.776+09:00 INFO 10959 --- [io-8080-exec-10] p6spy : #1763794326776 | took 4ms | statement | connection 5| url jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8
insert into movie_reservation (movie_title,username) values (?,?)
insert into movie_reservation (movie_title,username) values ('짱구는 못말려 극장판','테스터유저');
2025-11-22T15:52:06.784+09:00 INFO 10959 --- [io-8080-exec-10] p6spy : #1763794326784 | took 3ms | statement | connection 5| url jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8
insert into reservation_log (message) values (?)
insert into reservation_log (message) values ('철현 예약 성공 로그 기록 시도');
2025-11-22T15:52:06.784+09:00 INFO 10959 --- [io-8080-exec-10] i.t.d.m.service.ReservationLogService : [영화 예매 로그 저장]논리 트랜잭션에서 예외 발생, rollback-only 표기
2025-11-22T15:52:06.785+09:00 INFO 10959 --- [io-8080-exec-10] i.t.domain.movie.service.MovieService : 논리 트랜잭션 예외 catch 처리 완료 -> 과연 커밋이 될까?
outer에서 로그 예외 캐치: 로그 저장 중 에러!
2025-11-22T15:52:06.785+09:00 INFO 10959 --- [io-8080-exec-10] i.t.domain.movie.service.MovieService : === 영화 예매 완료 ===
2025-11-22T15:52:06.787+09:00 INFO 10959 --- [io-8080-exec-10] p6spy : #1763794326787 | took 2ms | rollback | connection 5| url jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8
;
2025-11-22T15:52:06.792+09:00 ERROR 10959 --- [io-8080-exec-10] i.t.c.e.GlobalExceptionRestAdvice : [500] POST /api/v1/movie/reserve - Transaction silently rolled back because it has been marked as rollback-only
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:804) ~[spring-tx-6.2.11.jar:6.2.11]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:758) ~[spring-tx-6.2.11.jar:6.2.11]
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:698) ~[spring-tx-6.2.11.jar:6.2.11]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:416) ~[spring-tx-6.2.11.jar:6.2.11]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.11.jar:6.2.11]
propagation = Propagation.REQUIRES_NEW 전파 속성을 주어 별도의 물리 트랜잭션을 구성하도록 하면 된다.@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/movie")
@Tag(name = "영화예매", description = "영화예매 트랜잭션 테스트 API")
public class MovieController {
private final MovieService movieService;
@PostMapping("/reserve")
@Operation(summary = "REQUIRES_NEW + try-catch 없음", description = "자식이 REQUIRES_NEW이고 부모에서 catch 안 했을 때 동작 확인")
public String reserve() {
movieService.reserveWithLog();
return "OK";
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class MovieService {
private final MovieReservationService reservationService;
private final ReservationLogService logService;
@Transactional
public void reserveWithLog() {
reservationService.reserve("짱구는 못말려 극장판", "테스터유저");
// REQUIRES_NEW인데 try-catch 안 함
// → 자식 트랜잭션은 독립적으로 롤백되지만
// → 예외가 그대로 부모로 전파되어 부모 트랜잭션도 롤백됨
logService.saveLog("철현 예약 성공 로그 기록 시도");
log.info("=== 이 로그는 출력되지 않음 (위에서 예외 터져서) ===");
}
}
@Service
@RequiredArgsConstructor
public class MovieReservationService {
private final MovieReservationRepository reservationRepository;
@Transactional
public void reserve(String movieTitle, String username) {
MovieReservation reservation = new MovieReservation();
reservation.setMovieTitle(movieTitle);
reservation.setUsername(username);
reservationRepository.save(reservation);
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class ReservationLogService {
private final ReservationLogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
ReservationLog reservationLog = new ReservationLog();
reservationLog.setMessage(message);
logRepository.save(reservationLog);
if (true) {
log.info("[REQUIRES_NEW] 자식 트랜잭션에서 예외 발생 → 자식만 롤백");
throw new RuntimeException("로그 저장 중 에러!");
}
}
}
2026-04-26 16:20:11.382 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188011382 | took 8ms | statement | connection 4| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
insert into movie_reservation (movie_title,username) values (?,?)
insert into movie_reservation (movie_title,username) values ('짱구는 못말려 극장판','테스터유저');
2026-04-26 16:20:11.442 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188011442 | took 9ms | statement | connection 5| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
insert into reservation_log (message) values (?)
insert into reservation_log (message) values ('철현 예약 성공 로그 기록 시도');
2026-04-26 16:20:11.443 INFO [http-nio-8080-exec-1] [MOVIE] i.t.d.m.s.ReservationLogService - [REQUIRES_NEW] 자식 트랜잭션에서 예외 발생 → 자식만 롤백
2026-04-26 16:20:11.449 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188011449 | took 4ms | rollback | connection 5| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
;
2026-04-26 16:20:11.454 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188011454 | took 1ms | rollback | connection 4| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
;
2026-04-26 16:20:11.458 ERROR [http-nio-8080-exec-1] [MOVIE] i.t.c.e.GlobalExceptionRestAdvice - [SERVER_EXCEPTION] POST /api/v1/movie/reserve - 로그 저장 중 에러!
java.lang.RuntimeException: 로그 저장 중 에러!
at io.sample.domain.movie.service.ReservationLogService.saveLog(ReservationLogService.java:26)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:360)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)
at io.sample.common.aop.DomainLogAspect.handleDomainLog(DomainLogAspect.java:27)
..
2026-04-26 16:20:11.491 WARN [http-nio-8080-exec-1] [ETC] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver - Resolved [java.lang.RuntimeException: 로그 저장 중 에러!]
@Service
@RequiredArgsConstructor
@Slf4j
public class MovieService {
private final MovieReservationService reservationService;
private final ReservationLogService logService;
@Transactional
public void reserveWithLog() {
reservationService.reserve("짱구는 못말려 극장판", "테스터유저");
// REQUIRES_NEW → 자식은 독립 트랜잭션
// 부모에서 catch → 예외 전파 차단 → 부모 트랜잭션 정상 커밋
try {
logService.saveLog("철현 예약 성공 로그 기록 시도");
} catch (Exception e) {
log.info("자식 트랜잭션 롤백됨, 부모는 계속 진행. 예외: {}", e.getMessage());
}
log.info("=== 부모 트랜잭션 정상 커밋 ===");
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class ReservationLogService {
private final ReservationLogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
ReservationLog reservationLog = new ReservationLog();
reservationLog.setMessage(message);
logRepository.save(reservationLog);
if (true) {
log.info("[REQUIRES_NEW] 자식 트랜잭션에서 예외 발생 → 자식만 롤백");
throw new RuntimeException("로그 저장 중 에러!");
}
}
}
2026-04-26 16:29:59.211 INFO [http-nio-8080-exec-1] [ETC] o.s.web.servlet.DispatcherServlet - Completed initialization in 10 ms
2026-04-26 16:29:59.245 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188599245 | took 3ms | statement | connection 12| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
insert into movie_reservation (movie_title,username) values (?,?)
insert into movie_reservation (movie_title,username) values ('짱구는 못말려 극장판','테스터유저');
2026-04-26 16:29:59.258 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188599258 | took 4ms | statement | connection 13| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
insert into reservation_log (message) values (?)
insert into reservation_log (message) values ('철현 예약 성공 로그 기록 시도');
2026-04-26 16:29:59.259 INFO [http-nio-8080-exec-1] [MOVIE] i.t.d.m.s.ReservationLogService - [REQUIRES_NEW] 자식 트랜잭션에서 예외 발생 → 자식만 롤백
2026-04-26 16:29:59.262 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188599262 | took 1ms | rollback | connection 13| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
;
2026-04-26 16:29:59.265 INFO [http-nio-8080-exec-1] [MOVIE] i.t.d.movie.service.MovieService - 자식 트랜잭션 롤백됨, 부모는 계속 진행. 예외: 로그 저장 중 에러!
2026-04-26 16:29:59.265 INFO [http-nio-8080-exec-1] [MOVIE] i.t.d.movie.service.MovieService - === 부모 트랜잭션 정상 커밋 ===
2026-04-26 16:29:59.269 INFO [http-nio-8080-exec-1] [MOVIE] p6spy - #1777188599269 | took 3ms | commit | connection 12| url jdbc:mysql://localhost:3306/sample?characterEncoding=UTF-8
;