[Spring] 스프링 DB 2편 - 데이터 접근 핵심 원리 섹션10 스프링 트랜잭션 전파1 - 기본
⬜ 섹션10 스프링 트랜잭션 전파1 - 기본
◼️ 스프링 트랜잭션 전파1 - 커밋, 롤백
@Slf4j
@SpringBootTest
public class BasicTxTest {
@Autowired
PlatformTransactionManager txManager;
@TestConfiguration
static class Config {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Test
void commit() {
log.info("트랜잭션 시작");
TransactionStatus status = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("트랜잭션 커밋 시작");
txManager.commit(status);
log.info("트랜잭션 커밋 완료");
}
@Test
void rollback() {
log.info("트랜잭션 시작");
TransactionStatus status = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("트랜잭션 롤백 시작");
txManager.rollback(status);
log.info("트랜잭션 롤백 완료");
}
}
◼️ 스프링 트랜잭션 전파2 - 트랜잭션 두 번 사용
트랜잭션이 각각 따로 사용되는 경우(트랜잭션1→트랜잭션2)를 추가로 보자.
@Test
void double_commit() {
log.info("트랜잭션1 시작");
TransactionStatus tx1 = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("트랜잭션1 커밋");
txManager.commit(tx1);
log.info("트랜잭션2 시작");
TransactionStatus tx2 = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("트랜잭션2 커밋");
txManager.commit(tx2);
}
위를보면, 트랜잭션1,2가 conn0커넥션을 획득하지만 이는 서로 다른 커넥션이다.
트랜잭션1이 conn0커넥션 사용후 커넥션 풀에 반납 후 트랜잭션 2가 conn0 커넥션을 커넥션 풀에서 획득한 것이다. (커넥션 재사용)
즉, 트랜잭션이 각각 수행되므로 사용되는 DB 커넥션도 각각 다르다.
◼️ 스프링 트랜잭션 전파3 - 전파 기본
🟢물리 & 논리 트랜잭션
- 물리 트랜잭션: 실제 DB에 적용되는 트랜잭션
- 논리 트랜잭션: 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위
🟢트랜잭션 원칙
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
◼️ 스프링 트랜잭션 전파4 - 전파 예제
외부 트랜잭션 수행중 내부 트랜잭션을 추가로 수행해보자.
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); //true
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
//트랜잭션 매니저는 트랜잭션 생성결과를 TransactionStatus에 담아 반환한다.
//isNewTransaction를 통해 신규 트랜잭션 여부를 확인할 수 있다.
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); //false
log.info("내부 트랜잭션 커밋");
txManager.commit(inner);
log.info("외부 트랜잭션 커밋");
txManager.commit(outer);
}
위에서, 외부 태랜잭션과 내부트랜잭션이 하나의 물리 트랜잭션으로묶이게 된다.
이때, 하나의 커넥션에 커밋은 한번만 호출할 수 있다. 따라서 처음 트랜잭션을 시작한 외부 트랜잭션이 실제 물리 트랜잭션을 관리하도록 한다. (위 결과를 보면 '내부 트랜잭션 커밋'후 로그가 없다.사실 내부 커밋은 아무동작도 하지않는다.)
로직 1 사용후, 커넥션이 필요한 경우 트랜잭션 동기화 매니저를 통해 트랜잭션이 적용된 커넥션을 획득해서 사용한다.
내부 트랜잭션을 시작할때, 기존 트랜잭션이 존재하므로,기존 트랜잭션에 참여한다.(사실 아무것도 하지않는다는 뜻...)
🟢핵심 정리
- 트랜잭션 매니저에 커밋을 호출한다고해서 항상 실제 커넥션에 물리 커밋이 발생하지는 않는다.
- 신규 트랜잭션인 경우에만 실제 커넥션을 사용해서 물리 커밋과 롤백을 수행한다. 신규 트랜잭션이 아니면 실제
물리 커넥션을 사용하지 않는다. - 트랜잭션이 내부에서 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고, 모든 논리 트랜잭션이
커밋되면 물리 트랜잭션이 커밋된다고 이해하면 된다.
◼️ 스프링 트랜잭션 전파5 - 외부 롤백
이번엔 내부 트랜잭션은 커미소디는데,외부 트랜잭션은 롤백되는 상황을 보자.
논리 트랜잭션이 하나라도 롤백되면 전체 물리 트랜잭션은 롤백된다.
@Test
void outer_rollback() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("내부 트랜잭션 커밋");
log.info("외부 트랜잭션 롤백");
txManager.rollback(outer);
}
내부 커밋은 실제 커밋을 호출하지않는다. 그 후, 외부 트랜잭션이 물리 트랜잭션 시작 후 롤백한다. 외부 트랜잭션은 신규 트랜잭션이므로 DB 커넥션에 실제 롤백을 호출한다.
◼️ 스프링 트랜잭션 전파5 - 내부 롤백
이번엔 내부 트랜잭션 롤백, 외부 트랜잭션 커밋 상황을 보자.
내부 트랜잭션은 물리 트랜잭션에 영향을 미치지못하므로 이는 앞선 상황들과 케이스가 조금 다르다.
@Test
void inner_rollback() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("내부 트랜잭션 시작"); // 기존 트랜잭션에 참여
TransactionStatus inner = txManager.getTransaction(new
DefaultTransactionAttribute());
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner); //내부 트랜잭션을 롤백하면 실제 물리 트랜잭션은 롤백하지 않는다.
//대신 기존 트랜잭션을 롤백 전용으로 표시한다.
log.info("외부 트랜잭션 커밋"); // [log] Global transaction is marked as rollback-only
assertThatThrownBy(() -> txManager.commit(outer))
.isInstanceOf(UnexpectedRollbackException.class);
}
내부 트랜잭션 롤백시,실제 물리 트랜잭션은 롤백하지 않고 대신 기존 트랜잭션에 '롤백 전용'을 표시한다.
그후 외부 트랜잭션이 커밋될때, '롤백 전용' 표시가 있다면 물리 트랜잭션을 롤백한다.
◼️ 스프링 트랜잭션 전파7 - REQUIRES_NEW
이번엔 외부,내부 트랜잭션을 완전히 분리해 사용해보자. 커밋과 롤백도 각각 별도로 이루어진다.
PROPAGATION_REQUIRES_NEW 옵션을 사용하면 기존 트랜잭션에 참여하는 것이 아니라, 새로운 물리 트랜잭션을 만들어서 시작하게 된다. 내부 외부는 각각 conn0,conn1 다른 커넥션을 사용한다.
이렇듯 이 옵션을 사용하면 DB 커넥션이 동시에 2개 사용된다는 점을 주의해야 한다!
@Test
void inner_rollback_requires_new() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); //true
log.info("내부 트랜잭션 시작");
DefaultTransactionAttribute definition = new DefaultTransactionAttribute();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus inner = txManager.getTransaction(definition);
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); //true
log.info("내부 트랜잭션 롤백");
txManager.rollback(inner); //롤백
log.info("외부 트랜잭션 커밋");
txManager.commit(outer); //커밋
}
◼️ 스프링 트랜잭션 전파8 - 다양한 전파 옵션
이실무에선 대부분 REQUIRED 기본 옵션을 사용하고, 아주 가끔 REQUIRED_NEW 옵션을 사용, 나머지는 거의 사용하지 않는다.
또한 isolation,timeout,readOnly는 트랜잭션이 처음 시작될 때만 적용되고 트랜잭션에 참여하는 경우에는 적용되지않는다.
🔶옵션 정리
REQUIRED
가장 많이 사용하는 기본 설정이다. 기존 트랜잭션이 없으면 생성하고, 있으면 참여한다. 트랜잭션이 필요하다는 의미로 이해하면 된다. (필수이기 때문에 없으면 만들고, 있으면 참여한다.)
- 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.
REQUIRES_NEW
항상 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 있음: 새로운 트랜잭션을 생성한다.
SUPPORTS
트랜잭션을 지원한다는 뜻이다. 기존 트랜잭션이 없으면, 없는대로 진행하고, 있으면 참여한다.
- 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
- 기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.
NOT_SUPPORTED
트랜잭션을 지원하지 않는다는 의미이다.
- 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
- 기존 트랜잭션 있음: 트랜잭션 없이 진행한다. (기존 트랜잭션은 보류한다)
MANDATORY
의무사항이다. 트랜잭션이 반드시 있어야 한다. 기존 트랜잭션이 없으면 예외가 발생한다.
- 기존 트랜잭션 없음: IllegalTransactionStateException 예외 발생
- 기존 트랜잭션 있음: 기존 트랜잭션에 참여한다.
NEVER
트랜잭션을 사용하지 않는다는 의미이다. 기존 트랜잭션이 있으면 예외가 발생한다. 기존 트랜잭션도 허용하지 않는 강한 부정의 의미로 이해하면 된다.
- 기존 트랜잭션 없음: 트랜잭션 없이 진행한다.
- 기존 트랜잭션 있음: IllegalTransactionStateException 예외 발생
NESTED
- 기존 트랜잭션 없음: 새로운 트랜잭션을 생성한다.
- 기존 트랜잭션 있음: 중첩 트랜잭션을 만든다.
- 중첩 트랜잭션은 외부 트랜잭션의 영향을 받지만, 중첩 트랜잭션은 외부에 영향을 주지 않는다.
- 중첩 트랜잭션에서 롤백되어도 외부 트랜잭션은 커밋할 수 있다.
- 외부 트랜잭션이 롤백 되면 중첩 트랜잭션도 함께 롤백된다.
- 참고: JDBC savepoint 기능을 사용한다. DB 드라이버에서 해당 기능을 지원하는지 확인이 필요하다.
- 중첩 트랜잭션은 JPA에서는 사용할 수 없다.