이 글은 해당 글을을 번역한 것입니다.
어떻게 JDBC Transaction을 시작하고, 커밋 혹은 롤백을 할까?
Spring의 @Transactional annotation, 일반 Hibernate, jOOQ 또는 기타 데이터베이스 라이브러리를 사용하던지 간에, 결국 dababase transaction을 열고 닫기 위해 동일한 동작을 수행한다.
기본적인 JDBC Transaction은 다음과 같다.
import java.sql.Connection;
Connection connection = dataSource.getConnection(); // (1)
try (connection) {
connection.setAutoCommit(false); // (2)
// execute some SQL statements...
connection.commit(); // (3)
} catch (SQLException e) {
connection.rollback(); // (4)
}
- Transanction을 시작하려면, database 에 연결이 필요하다.
- 명칭은 약간 생소할 수 있지만, java에서 database transaction을 시작할 수 있는 유일한 방법이다.
setAutoCommit(true) 는 모든 단일 SQL 문이 자동으로 자체 트랜잭션에 wrapping되도록 하고 setAutoCommit(false) 는 그 반대이다.
: master transaction 내의 트랜잭션 작업 단위가 반복적으로 commit 메서드를 호출하지 않도록 하기 위해 false를 사용- Transaction 커밋.
- 만약 exception이 발생한다면 rollback
위의 4 줄이 @Transaction annotation을 사용할 때마다, 스프링이 해주는 모든 것이다.(단순화 시켜서)
JDBC의 작동원리를 바탕으로, Spring의 transaction을 알아보자.
일반 JDBC를 사용하면 트랜잭션을 Management하는 방법이 단 하나 setAutocommit(false)) 인 반면, Spring은 동일한 작업을 수행할 수 있는 여러 가지 더 편리한 방법
을 제공합니다.
Spring에서 트랜잭션을 정의하는 방식이지만, 다소 드물게 사용되는 방법은 프로그래밍 방식으로, TransactionTemplate을 통하거나 직접 PlatformTransactionManager를 통하는 것이다.
코드로 보면 다음과 같다.
@Service
public class UserService {
@Autowired
private TransactionTemplate template;
public Long registerUser(User user) {
Long id = template.execute(status -> {
// execute some SQL that e.g.
// inserts the user into the db and returns the autogenerated id
return id;
});
}
}
일반 JDBC 와 비교했을 때,
- 데이터베이스 연결을 열거나 닫는 작업을 할 필요가 없다. 대신 Transaction Callbacks 를 사용해야 한다.
- 스프링은 exception이 발생했을 때, runtime exception으로 변환해주기 때문에, SQLException을 catch 할 필요가 없다.
- 스프링 생태계에 효과적으로 통합되기 때문에, 많은 Spring context의 configuration에서 필요하지만, 더 이상 추가적인 작업을 하지 않아도 된다.
다음은 Spring transaction management가 일반적으로 활용되는 방법이다.
public class UserService {
@Transactional
public Long registerUser(User user) {
// execute some SQL that e.g.
// inserts the user into the db and retrieves the autogenerated id
// userDao.save(user);
return id;
}
}
더 이상 다른 구성이 필요하지 않지만, 다음과 같은 2 가지 작업이 필요하다.
그러면 @Transactional annotation을 추가한 모든 빈의 public method 에 database transaction이 실행된다.
즉 @Transactional을 사용하려면 다음과 같은 transaction manament를 작성해주면 된다.
@Configuration
@EnableTransactionManagement
public class MySpringConfig {
@Bean
public PlatformTransactionManager txManager() {
return yourTxManager; // more on that later
}
}
Spring에는 프록시를 다루기 위한 이점이 있는데, 그 핵심은 IoC 컨테이너이다. 컨테이너는 UserService를 인스턴스화하고 해당 UserService를 UserService가 필요한 다른 빈에 DI한다.
이제 빈에서 @Transactional을 사용할 때마다 Spring은 작은 트릭
을 사용한다. UserService뿐만 아니라 해당 UserService의 트랜잭션 프록시
도 인스턴스화합니다 .
위의 과정은 Cglib 라이브러리의 도움으로 서브클래싱을 통한 프록시 수행된다. ( Dynamic JDK 프록시 와 같은) 프록시를 구성하는 다른 방법도 있음
위의 과정을 이미지로 풀면 다음과 같다.
데이터베이스 연결/트랜잭션 열기 및 닫기.
그런 다음 작성한 실제 UserService 에 위임
그리고 UserRestController와 같은 다른 bean은 실제(target) 가 아니라 프록시와 호출하고 있다는 것을 알지 못한다.
다음 두 트랜잭션 클래스가 있다고 가정하자.
@Service
public class UserService {
@Autowired
private InvoiceService invoiceService;
@Transactional
public void invoice() {
invoiceService.createPdf();
// send invoice as email, etc.
}
}
@Service
public class InvoiceService {
@Transactional
public void createPdf() {
// ...
}
}
UserService에는 invoice() 메서드가 있고
InvoiceService에서 다른 트랜잭션 메서드인 createPdf()를 호출한다.
데이터베이스 트랜잭션의 관점에서 이 작업은 실제로 하나의 데이터베이스 트랜잭션이어야 한다.
(위의 개념을 기억하자: getConnection(). setAutocommit(false). commit(). )
Spring은 이 물리적 트랜잭션을 호출한다.
그러나 Spring에서는 두 가지 논리적 트랜잭션이 발생한다. 첫 번째는 UserService에서, 다른 하나는 InvoiceService에서 발생한다. Spring은 두 @Transactional 메소드가 동일한 기본 물리적 데이터베이스 트랜잭션을 사용해야 한다는 것을 알 만큼 영리하다.
InvoiceService가 다음과 같이 변경되면 상황이 어떻게 달라질까?
@Service
public class InvoiceService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
propagation(전파) 모드를 require_new
로 변경하면 이미 존재하는 다른 트랜잭션과 독립적으로
createPDF()가 자체 트랜잭션에서 실행되어야 한다고 Spring에 알린다.
위의 코드는 데이터베이스에 대한 두 개의
(물리적) connections/transactions을 열 것임을 의미한다. (즉: getConnection() x2. setAutocommit(false) x2. commit() x2 ) 이제 Spring은 두 개의 논리적 트랜잭션 조각(invoice()/createPdf())이 이제 두 개의 서로 다른 물리적 데이터베이스에 매핑된다.
요약하자면
물리적 트랜잭션: 실제 JDBC 트랜잭션
논리적 트랜잭션: (잠재적으로 중첩된) @Transactional-annotated(Spring) 메서드
Spirng을 처음 시작하는 사람들이 일반적으로 겪는 한 가지 이슈이다. 다음과 같은 코드가 있다고 가정하자.
@Service
public class UserService {
@Transactional
public void invoice() {
createPdf();
// send invoice as email, etc.
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
invoice()에서 트랜잭션이기도 한 createPDF()를 호출했을 때
누군가가 invoke()를 호출하면 실제 트랜잭션이 몇 개나 열릴까?
답은 한 개
이다.
왜냐하면, Spring은 트랜잭션 UserService 프록시를 생성하지만 일단 UserService 클래스 내부에 있고 다른 내부 메소드를 호출하면 더 이상 프록시가 관련되지 않는다. 즉, 새로운 transaction이 생성되지 않는다.
그림으로 보면 다음과 같다.
먼저 순수 Hibernate의 사용법을 보면 다음과 같다.
public class UserService {
@Autowired
private SessionFactory sessionFactory; // (1)
public void registerUser(User user) {
Session session = sessionFactory.openSession(); // (2)
// lets open up a transaction. remember setAutocommit(false)!
session.beginTransaction();
// save == insert our objects
session.save(user);
// and commit it
session.getTransaction().commit();
// close the session == our jdbc connection
session.close();
}
}
- 모든 Hibernate 쿼리에 대한 진입점인(entry point) Hibernate SessionFactory
- Hibernate의 API를 사용하여 세션(읽기: 데이터베이스 연결) 및 트랜잭션을 수동으로 관리합니다.
그러나 위의 코드에는 한 가지 큰 문제가 있다.
그러나 @Transactional 을 사용함으로써, spring과 hibernate를 통합할 수 있다. 즉, 서로의 트랜잭션에 대해 알 수 있다.
코드를 보면
@Service
public class UserService {
@Autowired
private SessionFactory sessionFactory; // (1)
@Transactional
public void registerUser(User user) {
sessionFactory.getCurrentSession().save(user); // (2)
}
}
- 이전과 동일한 SessionFactory
- getCurrentSession() 및 @Transactional이 동기화 되기 때문에, 더 이상 수동 상태 관리는 필요하지 않다.
이러한 통합 절차는 Spring 구성에서 DataSourcePlatformTransactionManager 를 사용하는 대신에 HibernateTransactionManager (일반 Hibernate를 사용하는 경우) 또는 JpaTransactionManager (JPA를 통해 Hibernate를 사용하는 경우)를 사용함으로써 가능하다.
그림으로 단순화해서 표현할 경우, 다음과 같은 방법으로 통합된다.