[인프런 김영한] 스프링 트랜잭션 AOP, 커밋/롤백, 전파
|2026. 2. 18. 22:23
트랜잭션 AOP 이해

- @Transactional이 적용된 특정 메서드나 클래스가 하나라도 있다면, 트랜잭션 AOP는 프록시를 만들어서 스프링 컨테이너에 등록한다.
- 그리고 프록시가 내부에서 실제 basicService를 참조하게 된다.

- 만약 호출되는 함수가 트랜잭션 적용 대상이라면, 트랜잭션을 먼저 시작하고 basicService.tx()를 호출한다.
- 트랜잭션 적용 대상이 아니라면, 그냥 바로 basicSerivec.nonTx()를 호출한다.
트랜잭션 AOP 초기화 시점 주의사항
@SpringBootTest
public class InitTxTest {
@Autowired
Hello hello;
@Test
void go() {
}
@TestConfiguration
static class InitTxTestConfig {
@Bean
Hello hello() {
return new Hello();
}
}
@Slf4j
static class Hello {
@PostConstruct
@Transactional
public void initV1() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init @PostConstruct tx active = {}", isActive);
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Hello init ApplicationReadyEvent tx active = {}", isActive);
}
}
}
- 초기화 시점에는 트랜잭션 AOP가 적용이 되지 않을 수 있다. 예를 들어, @PostConstruct와 @Transactional을 함께 사용하면 트랜잭션이 적용되지 않는다.
- 초기화 코드가 먼저 호출되고 그 다음에 트랜잭션 AOP가 적용되기 때문이다.
- 따라서 ApplicationReadyEvent 이벤트를 통해, AOP를 포함한 스프링 컨테이너가 완전히 생성되고 난 다음에 트랜잭션 AOP가 적용될 수 있도록 한다.
스프링 트랜잭션 옵션
- readOnly = true로 설정하면, 커밋 시점에 플러시를 호출하지 않으며 변경감지를 위한 스냅샷 또한 1차 캐시에 저장해두지 않는 식으로 최적화가 된다.
- 또한, JDBC 드라이버와 DB에서도 읽기 전용인 경우, 내부에서 성능 최적화가 발생한다.
예외와 트랜잭션 커밋, 롤백
@SpringBootTest
public class RollbackTest {
@Autowired
RollbackService rollbackService;
@Test
void runtimeException() {
Assertions.assertThatThrownBy(() -> rollbackService.runTimeException())
.isInstanceOf(RuntimeException.class);
}
@Test
void checkedException() {
Assertions.assertThatThrownBy(() -> rollbackService.checkedException())
.isInstanceOf(MyException.class);
}
@Test
void rollbackForCheckedException() {
Assertions.assertThatThrownBy(() -> rollbackService.rollbackFor())
.isInstanceOf(MyException.class);
}
@TestConfiguration
static class RollbackTestConfig {
@Bean
RollbackService rollbackService() {
return new RollbackService();
}
}
@Slf4j
static class RollbackService {
// 런타임 예외: 롤백
@Transactional
public void runTimeException() {
log.info("call runTimeException");
throw new RuntimeException();
}
// 체크 예외: 커밋
@Transactional
public void checkedException() throws MyException {
log.info("call checkedException");
throw new MyException();
}
// 체크 예외 rollbackFor 지정: 롤백
@Transactional(rollbackFor = MyException.class)
public void rollbackFor() throws MyException {
log.info("call rollbackFor checkedException");
throw new MyException();
}
}
static class MyException extends Exception {
}
}
- 스프링은 기본적으로 언체크 예외인 RuntimeException, Error와 그 하위 예외는 롤백하고, 체크 예외인 Exception과 그 하위 예외들은 커밋한다.
- 스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 언체크(런타임) 예외는 복구 불가능한 예외로 가정한다.
- 예를 들어, 고객의 잔고가 부족한 상황은 시스템은 정상 동작했지만 비즈니스 상황이 예외인 것으로, 이러한 비즈니스 예외는 매우 중요하고 반드시 처리해야 하는 경우가 많으므로 체크 예외를 고려할 수 있다.
- 즉, 해당 비즈니스 예외가 발생하면 롤백하지 않고 커밋시키고(비즈니스 상황상, 변경사항이 db에 반영되어야 하므로) 별도 상태 컬럼을 변경하여 추후에 처리하는 식으로 활용 가능하다.
- 따라서 이러한 이유로 인해, 스프링은 언체크 예외는 롤백하고 체크 예외는 커밋한다.
- rollbackFor 옵션을 사용하면, 어떤 예외가 발생할 때 롤백을 할 지 지정할 수 있다
- noRollbackFor은 rollBackFor과 반대로, 어떤 예외가 발생했을 때 롤백하면 안되는지 지정할 수 있다.
스프링 트랜잭션 전파

- 한 외부 트랜잭션에서 또 다른 내부 트랜잭션이 시작되면, 스프링은 둘을 묶어서 하나의 트랜잭션으로 만들어준다.
- 내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다. 이것이 기본 동작이다.
- 여기서, 하나의 트랜잭션으로 묶은 트랜잭션을 물리 트랜잭션이라고 하고, 물리 트랜잭션에 묶인 각각의 트랜잭션을 논리 트랜잭션이라고 한다.

- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋될 수 있다.


- 한 트랜잭션 내에서는 커밋 혹은 롤백은 한 번씩만 호출될 수 있다.
- 따라서, 내부 트랜잭션은 별도 커넥션을 얻어 트랜잭션을 시작하지 않고 외부 트랜잭션이 시작한 트랜잭션에 참여하기 위해 아무것도 하지 않는다. 또한, 커밋해도 DB 커넥션에 실제 커밋을 하지 않는다.
- 외부 트랜잭션만 물리 트랜잭션을 시작하고 DB 커넥션에 커밋한다.
- 즉, 트랜잭션 매니저에 커밋을 호출한다고 해서 항상 실제 커넥션에 물리 커밋이 발생하지는 않는다는 것이다.
- 신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행하는 것이다.
스프링 트랜잭션 전파 - 롤백

- 내부 트랜잭션은 커밋되고 외부 트랜잭션은 롤백된다면, 전체 물리 트랜잭션이 롤백된다.

- 내부 트랜잭션은 롤백되면 실제 물리 트랜잭션을 롤백할 수 없다. 대신 기존 트랜잭션을 롤백 전용으로 표시한다. (트랜잭션 동기화 매니저에 rollbackOnly=true로)
- 때문에 외부 트랜잭션 커밋 시점에, 전체 트랜잭션이 롤백 전용으로 표시되어 있기 때문에 전체 트랜잭션을 롤백한다.
- 그리고 UnexpectedRollbackException 예외를 던진다. 외부 트랜잭션은 커밋을 호출했는데, 실제로는 커밋이 되지 않고 롤백되었기 때문에 해당 사실을 명확하게 알려주기 위함이다.
REQUIRES_NEW

- 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법이다.
- 따라서 내부 트랜잭션은 외부 트랜잭션에 영향을 주지 못한다.
- 외부 트랜잭션이 시작되어 con1을 사용하다가 내부 트랜잭션이 시작되면 con1 사용을 보류하고 con2를 사용한다. 내부 트랜잭션이 끝나서 con2를 다 사용하면 con1을 다시 사용한다.
스프링 트랜잭션 전파 - 복구

- 예외가 터졌을 때 내부에서 예외를 잡으면 해당 물리 트랜잭션은 롤백되지 않는다.
- 하지만, 내부에서 터진 예외를 외부에서 잡으면 rollbackOnly=true로 표시되어서 위처럼 물리 트랜잭션이 롤백된다.
- 즉, 예외가 내부 트랜잭션 프록시를 통과해서 밖으로 나오는 순간 이미 롤백은 확정된다.

- 하지만, REQUIRES_NEW를 사용하여 트랜잭션을 분리한다면 로그 저장이 실패하더라도 회원가입은 정상적으로 진행되는 비즈니스 요구사항을 만족할 수 있다.
- 즉, 논리 트랜잭션이 하나라도 롤백되면 관련 물리 트랜잭션 또한 롤백되므로, 트랜잭션을 분리해서 해결하는 것이다.
- 하지만, DB 커넥션을 더 많이 사용하기 때문에 성능이 중요한 곳에서는 이런 부분을 주의해서 사용해야 한다.
- 따라서 REQUIRES_NEW를 사용하지 않고 문제를 해결할 수 (구조 변경 등) 있다면 해당 방법을 사용하는 것이 더 좋다.

- 위처럼 구조를 바꾸면 DB 커넥션을 순차적으로 사용하기 때문에 커넥션을 하나만 사용하면서, REQUIRES_NEW를 사용하지 않고도 문제를 해결할 수 있다.
- 하지만 구조상 REQUIRES_NEW를 사용하는 것이 더 깔끔한 경우도 있으므로, 트레이드오프를 이해해서 적절히 사용하면 된다.
'공부 > Spring' 카테고리의 다른 글
| OAuth 2.0과 JWT를 활용한 Spring Security 로그인 개념 정리 (0) | 2026.02.23 |
|---|---|
| 재고 차감으로 알아보는 동시성 제어 - DB, Redis 락 (0) | 2026.02.21 |
| [인프런 김영한] 자바 예외 (0) | 2026.02.08 |
| [인프런 김영한] 스프링과 트랜잭션 (0) | 2026.02.05 |
| [인프런 김영한] 커넥션 풀과 트랜잭션 (1) | 2026.02.01 |
