@Transactional이 클래스에 있든 메서드에 있든 있으면
클래스 전체를 그대로 상속받은 프록시가 생성되어 스프링 빈으로 등록된다. 그리고 프록시가 진짜 대신에 주입되고 클라이언트는 이 프록시객체를 사용하게 된다.
즉, 클라이언트가 트랜잭션이 적용된 서비스를 호출하면 프록시 객체가 트랜잭션, 커밋, 롤백 등의 부수적인 작업을 처리하고 실제 객체를 호출한다. 이를 Invocation
이라고 한다.
프록시 객체를 생성해서 사용하는 이유는 지연로딩을 구현해서 불필요한 데이터베이스 쿼리를 방지하고 성능을 최적화할 수 있습니다. 객체의 메서드 호출을 감싸서 보안과 관련된 작업을 수행할 수 있습니다. (사실 잘모르겠음)
JPA와는 달리 마이바티스는 트랜잭션 매니저를 bean으로 등록해주어야 트랜잭션을 사용할 수 있다.
<bean id="transactionManager" class="org.springframework.jdbc.datasource
.DataSourceTransactionManager">
<constructor-arg ref="dataSource" />
</bean>
@Configuration
public class DataSourceConfig {
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
프록시에서 트랜잭션을 처리한 후 실제 객체를 호출한다
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
public void external() {
log.info("call external");
printTxInfo();
internal(); // 트랜잭션이 적용되지 않은 실제 객체의 메서드에서 내부호출
}
이를 해결하기 위해 트랜잭션이 적용되는 메서드를 따로 분리해서 클래스를 만든다 그리고 만들어진 클래스는 주입해서 사용한다.
@Slf4j
static class InternalService {
@Transactional
public void internal() {
log.info("call internal");
printTxInfo();
}
@TestConfiguration
static class InternalCallV2Config {
@Bean
CallService callService() {
return new CallService(innerService());
}
@Bean
InternalService innerService() {
return new InternalService();
}
}
런타임 예외 발생 : rollback
체크 예외 발생 : commit
비즈니스 로직에서 예외가 발생한 경우 체크 예외로 처리하여 기존의 정보를 커밋하고 예외를 처리하는 방식. 이 경우에 런타임 예외를 발생시켜서 rollback이 일어나면 이전까지 해온 비즈니스가 모두 날아가기 때문에 체크예외를 사용한다. 비즈니스 상황에서 발생한 문제를 예외를 통해 알려주는 즉, 예외가 return값으로 활용된다.
체크예외 rollbackFor 지정 : rollback
@Transactional(rollbackFor = ) : 비즈니스 로직이지만 커밋이 아니라 롤백을 해야하는 상황인 경우 사용
트랜잭션이 진행 중인데 이 트랜잭션이 커밋이나 롤백을 하기 전에 내부에서(다른) 트랜잭션이 일어나는 경우로 외부와 내부 트랜잭션을 논리 트랜잭션
, 둘을 묶은 하나의 트랜잭션을 물리 트랜잭션
이라고 한다.
같은 물리 트랜잭션을 사용하는 경우에 같은 커넥션을 사용하게 된다.
모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
내부 트랜잭션은 외부 트랜잭션을 그대로 이어 받아 진행하므로 새로운 트랜잭션이 아니다. 내부 트랜잭션은 새로운 트랜잭션이 아니라서 트랜잭션은 하나뿐이다. 따라서 커밋이나 롤백은 딱 한 번만 가능한데 어떻게 모든 논리 트랜잭션이 커밋되어야 한다고 말할 수 있을까?
내부 트랜잭션이 물리 트랜잭션을 커밋하면 트랜잭션 전체가 종료되므로, 사실은 내부 트랜잭션에서 커밋을 호출해도 아무 일이 일어나지 않는다. 가장 처음 호출된 외부 트랜잭션의 커밋만 물리 트랜잭션을 관리할 수 있다.
내부 트랜잭션 코드와 트랜잭션 매니저는 하는 일이 거의 없기 때문에 로직1에서 로직 2로 넘어간다고 생각해도 무방하다.
- 내부 트랜잭션에서 롤백코드 실행시
Participating transaction failed - marking existing transaction
as rollback only
내부에서 롤백을 했는데 외부에서 커밋을 실행했을 시
Global transaction is marked as rollback-only
but transactional code requested commit
이 옵션을 사용하면 외부, 내부 트랜잭션이 각각 별도의 물리 트랜잭션과 커넥션을 가져서 서로의 커밋과 롤백에 영향을 주지 않는다. 아주 가끔 사용한다.
로직1에서 로직2로 넘어갈 때 커넥션1은 트랜잭션 동기화 매니저 안에 남아있는 상태로 다른 커넥션2가 사용된다.(커넥션 풀에 반납되는 것이 아님) 커넥션2에서 커밋 또는 롤백이 일어나면 커넥션2는 종료되고, 다시 커넥션 1이 사용된다.
service class {
private final RepositoryA a;
private final RepositoryB b;
public void save(){ a.save }
public void save(){ b.save }
}
- repositoryA class
@Transactional
public void save(){ em.persist }
- repositoryB class
@Transactional
public void save(){ em.persist }
@Transactional
service class {
private final RepositoryA a;
private final RepositoryB b;
public void save(){ a.save }
public void save(){ b.save }
}
- repositoryA class
public void save(){ em.persist }
- repositoryB class
public void save(){ em.persist }
여기서 예외가 발생한 트랜잭션은 새로운 트랜잭션(=가장 먼저 만들어진 트랜잭션)이 아니므로 롤백을 하지 않고 rollbackOnly
만 기록하고 예외를 던진다. 따라서 service단에서 예외를 잡더라도 물리트랜잭션은 기록된 rollbackOnly를 읽고 롤백을 해버린다.(UnExpectedRollbackException 발생)
REQUIRED_NEW
를 사용하면 항상 새로운 트랜잭션을 만들게되고 그 트랜잭션은 새로운 커넥션을 사용하게 된다. 신규 트랜잭션으로 인식되기 때문에 커밋과 롤백에 있어서 다른 물리트랜잭션과 별도로 작동한다.
주의해야할 점은 REQUIRED_NEW를 사용할 경우 하나의 HTTP 요청에 두 개이상의 커넥션을 사용하게 된다. 성능이 중요한 경우 조심해야한다.