1. Spring Transaction
- Spring Framework는 선언적 트랜잭션 관리와 프로그래밍 방식 트랜잭션 관리를 모두 지원합니다.
- 트랜잭션은 데이터베이스 작업의 논리적 단위로, ACID 속성(원자성, 일관성, 격리성, 지속성)을 보장합니다.
- Spring은 다양한 트랜잭션 관리자를 제공하여 JDBC, JPA, Hibernate 등 여러 기술과 통합할 수 있습니다.
- 기본적으로 PlatformTransactionManager 인터페이스를 통해 트랜잭션을 추상화합니다.
2. PlatformTransactionManager
2.1 PlatformTransactionManager의 필요성
- 각각의 데이터 접근 기술들은 트랜잭션을 처리하는 방식에 차이가 있습니다.
- JDBC 기술과 JPA 기술은 트랜잭션을 사용하는 코드 자체가 완전히 다릅니다.
- 아래는 두 기술의 트랜잭션 처리 방식 비교입니다.
JDBC 트랜잭션 코드 예시
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = null;
try {
con = dataSource.getConnection();
con.setAutoCommit(false); // 트랜잭션 시작
// 비즈니스 로직
bizLogic(con, fromId, toId, money);
con.commit(); // 성공시 커밋
} catch (Exception e) {
if (con != null) {
con.rollback(); // 실패시 롤백
}
throw new IllegalStateException(e);
} finally {
if (con != null) {
release(con);
}
}
}
JPA 트랜잭션 코드 예시
public static void main(String[] args) {
// 엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 생성
EntityTransaction tx = em.getTransaction(); // 트랜잭션 기능 획득
try {
tx.begin(); // 트랜잭션 시작
logic(em); // 비즈니스 로직
tx.commit(); // 트랜잭션 커밋
} catch (Exception e) {
tx.rollback(); // 트랜잭션 롤백
} finally {
em.close(); // 엔티티 매니저 종료
}
emf.close(); // 엔티티 매니저 팩토리 종료
}
스프링 트랜잭션 추상화 사용 코드 예시
public void accountTransfer(String fromId, String toId, int money) {
// 트랜잭션 매니저를 통한 일관된 방식의 트랜잭션 처리
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 비즈니스 로직 - JDBC 또는 JPA 등 다양한 데이터 접근 기술 사용 가능
bizLogic(fromId, toId, money);
transactionManager.commit(status); // 성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); // 실패시 롤백
throw new IllegalStateException(e);
}
}
- 위 코드에서 볼 수 있듯이, 스프링의 트랜잭션 추상화를 사용하면:
- 데이터 접근 기술과 무관하게 동일한 방식으로 트랜잭션을 관리할 수 있습니다.
- JDBC, JPA 등 다양한 데이터 접근 기술을 사용하더라도 트랜잭션 관리 코드는 변경할 필요가 없습니다.
- 데이터 접근 기술을 변경하더라도 트랜잭션 관리 코드는 그대로 유지됩니다.
2.2 PlatformTransactionManager 인터페이스
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
- 트랜잭션은 트랜잭션 시작(획득), 커밋, 롤백으로 단순하게 추상화 되어 있습니다.
- 스프링은 트랜잭션을 추상화해서 제공할 뿐만 아니라, 실무에서 주로 사용하는 데이터 접근 기술에 대한 트랜잭션 매니저의 구현체도 제공합니다.
- 예를 들어 JDBC의 경우
DataSourceTransactionManager
, JPA의 경우JpaTransactionManager
가 있습니다. - 스프링 부트는 어떤 데이터 접근 기술을 사용하는지를 자동으로 인식해서 적절한 트랜잭션 매니저 를 선택해서 스프링 빈으로 등록해줍니다.
- 때문에 트랜잭션 매니저를 선택하고 등록하는 과정도 생략할 수 있습니다.
정보
스프링 5.3부터는 JDBC 트랜잭션을 관리할 때 DataSourceTransactionManager
를 상속받아서 약간의 기능을 확장한 JdbcTransactionManager
를 제공합니다. 둘의 기능
차이는 크지 않으므로 같은 것으로 이해하셔도 됩니다.
2.3 PlatformTransactionManager 사용법
- PlatformTransactionManager을 사용하는 방법은 크게 두 가지로 나눌 수 있습니다.
- 프로그래밍 방식: 트랜잭션을 직접 관리하는 방법
- 선언적 방식: AOP를 사용하여 트랜잭션을 관리하는 방법
프로그래밍 방식
- 프로그래밍 방식은 트랜잭션을 직접 관리하는 방법으로, 코드에서 직접 트랜잭션을 시작하고 커밋하거나 롤백합니다.
- 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 직접 사용하여 트랜잭션을 관리합니다.
- 프로그래밍 방식은 애플리케이션 코드에 트랜잭션 관리를 직접 구현해야 하므로 코드가 복잡해질 수 있습니다.
- 애플리케이션 코드가 트랜잭션이라는 기술 코드와 강하게 결합됩니다.
- 따라서 선언적 트랜잭션 관리가 더 일반적으로 사용됩니다.
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); // 성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); // 실패시 롤백
throw new IllegalStateException(e);
}
- 위 코드에서 볼 수 있듯이, 프로그래밍 방식은 트랜잭션을 직접 관리하는 방식입니다.
- 트랜잭션을 시작하고, 비즈니스 로직을 실행한 후, 성공 시 커밋하고 실패 시 롤백합니다.
선언적 방식
- 선언적 트랜잭션 관리는 AOP(Aspect Oriented Programming)를 사용하여 트랜잭션을 관리하는 방법입니다.
- @Transactional 어노테이션을 사용하여 메서드나 클래스에 트랜잭션을 적용합니다.
- 스프링은 AOP를 사용하여 트랜잭션을 관리하므로, 코드에서 직접 트랜잭션을 관리할 필요가 없습니다.
- 이 방법은 코드의 가독성을 높이고 유지보수를 용이하게 합니다.
- 실무에서는 대부분 선언적 트랜잭션 관리를 사용합니다.
2.4 JPA와 JdbcTemplate 또는 Mybatis 함께 사용하기
- JPA, 스프링 데이터 JPA, Querydsl은 개발 생산성을 크게 향상시키지만, 학습 곡선이 높고 복잡한 통계 쿼리에는 적합하지 않을 수 있습니다.
- 복잡한 통계 쿼리가 필요한 경우 JdbcTemplate이나 MyBatis를 JPA와 함께 사용하는 방법이 좋은 대안입니다.
- 트랜잭션 관리 측면에서:
- JPA 기술들은 JpaTransactionManager를 사용합니다.
- JdbcTemplate, MyBatis는 DataSourceTransactionManager를 사용합니다.
- JpaTransactionManager는 DataSourceTransactionManager의 기능도 대부분 제공하기 때문에, JpaTransactionManager 하나만 등록하면 JPA, JdbcTemplate, MyBatis를 모두 하나의 트랜잭션으로 관리할 수 있습니다.
- 결과적으로 이 기술들을 함께 사용하면서도 트랜잭션 일관성을 유지할 수 있습니다.
위험
이렇게 JPA와 JdbcTemplate을 함께 사용할 경우 JPA의 플러시 타이밍에 주의해야 합니다. JPA는 데이터를 변경하면 변경 사항을 즉시 데이터베이스에 반영하지 않습니다. 기본적으로 트랜잭션이 커밋되는 시점에 변경 사항을 데이터베이스 에 반영합니다. 그래서 하나의 트랜잭션 안에서 JPA를 통해 데이터를 변경한 다음에 JdbcTemplate을 호출하는 경우 JdbcTemplate에서는 JPA가 변경한 데이터를 읽지 못하는 문제가 발생합니다. 이 문제를 해결하려면 JPA 호출이 끝난 시점에 JPA가 제공하는 플러시라는 기능을 사용해서 JPA의 변경 내역을 데이터 베이스에 반영해주어야 합니다. 그래야 그 다음에 호출되는 JdbcTemplate에서 JPA가 반영한 데이터를 사용할 수 있습니다.
3. 트랜잭션 동기화
- 스프링이 제공하는 트랜잭션 매니저는 크게 2가지 역할을 합니다.
- 트랜잭션 추상화: 트랜잭션을 시작하고 커밋하거나 롤백하는 기능을 추상화합니다.
- 리소스 동기화: 트랜잭션과 관련된 리소스를 동기화합니다.
- 트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야 합니다.
- 결국 같은 커넥션을 동기화(맞추어 사용)하기 위해서 이전에는 파라미터로 커넥션을 전달하는 방법을 사용했습니다.
- 파라미터로 커넥션을 전달하는 방법은 코드가 지저분해지는 것은 물론이고, 커넥션을 넘기는 메서드와 넘기지 않는 메서드를 중복해서 만들어야 하는 등 여러가지 단점들이 많습니다.
3.1 트랜잭션 동기화 매니저
org.springframework.transaction.support.TransactionSynchronizationManager
- 스프링은 트랜잭션 동기화 매니저를 제공합니다.
- 이것은 쓰레드 로컬(
ThreadLocal
)을 사용해서 커넥션을 동기화해줍니다. - 트랜잭션 매니저는 내부에서 이 트랜잭션 동기화 매니저를 사용합니다.
- 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화할 수 있습니다.
- 따라서 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득하면 됩니다.
- 따라서 이전처럼 파라미터로 커넥션을 전달하지 않아도 됩니다.
3.2 TransactionManager의 동작 방식
- 서비스 계층에서
transactionManager.getTransaction()
을 호출해서 트랜잭션을 시작합니다. - 트랜잭션을 시작하려면 먼저 데이터베이스 커넥션이 필요합니다. 트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션을 생성합니다.
- 커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션을 시작합니다.
- 커넥션을 트랜잭션 동기화 매니저에 보관합니다.
- 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관합니다. 따라서 멀티 쓰레드 환경에 안전하게 커넥션을 보관할 수 있습니다.
- 서비스는 비즈니스 로직을 실행하면서 리포지토리의 메서드들을 호출합니다. 이때 커넥션을 파라미터로 전달하지 않습니다.
- 리포지토리 메서드들은 트랜잭션이 시작된 커넥션이 필요합니다.
- 리포지토리는
DataSourceUtils.getConnection()
을 사용해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용합니다. - 이 과정을 통해서 자연스럽게 같은 커넥션을 사용하고, 트랜잭션도 유지됩니다.
- 획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행합니다.
- 리포지토리는
- 비즈니스 로직이 끝나고 트랜잭션을 종료합니다. 트랜잭션은 커밋하거나 롤백하면 종료됩니다.
- 트랜잭션을 종료하려면 동기화된 커넥션이 필요합니다. 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득합니다.
- 획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백합니다.
- 전체 리소스를 정리합니다.
- 트랜잭션 동기화 매니저를 정리합니다. 쓰레드 로컬은 사용후 꼭 정리해야 합니다.
con.setAutoCommit(true)
로 되돌립니다. 커넥션 풀을 고려해야 합니다.con.close()
를 호출하여 커넥션을 종료합니다. 커넥션 풀을 사용하는 경우con.close()
를 호출하면 커넥션 풀에 반환됩니다.