
스프링은 트랜잭션 추상화 기술을 제공한다. 데이터 접근 기술에 따른 트랜잭션 구현체도 대부분 제공하기 때문에 사용하기만 하면 된다.

PlatformTransactionManager : 스프링 트랜잭션 추상화의 핵심 인터페이스
-> org.springframework.transaction.PlatformTransactionManager
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
스프링이 구현체도 제공하기 때문에 구현체를 주입하여 사용하면된다. PlatformTransactionManager 인터페이스와 구현체를 포함해서 트랜잭션 매니저라고 줄여서 이야기 하도록 하자.
@RequiredArgsConstructor
@Slf4j
public class MemberServiceV3_1 {
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
//트랜잭션 시작 & release 자동
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//비지니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //로직 정상 수행시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {
//...
}
private void validation(Member toMember) {
//...
}
}
PlatformTransactionManager
getTransaction
트랜잭션 매니저는 트랜잭션 추상화 외에 리소스 동기화 역할도 수행한다. 트랜잭션을 유지하기 위해서는 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다. 이전에는 파라미터로 커넥션을 전달하는 방법을 사용했는데 많은 단점을 확인할 수 있었다.

TransactionSynchronizationManager : 트랜잭션 동기화 매니저
-> org.springframework.transaction.support.TransactionSynchronizationManager
1. 트랜잭션을 시작하려면 커넥션이 필요, 트랜잭션 매니저는 데이터소스를 통해 커넥션을 만들고 트랜잭션을 시작
2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관
3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용, 따라서 파라미터로 커넥션을 전달하지 않아도 됨
4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고, 커넥션도 닫음
@Slf4j
public class MemberRepositoryV3 {
private final DataSource dataSource;
public MemberRepositoryV3(DataSource dataSource) {
this.dataSource = dataSource;
}
// 1)데이터 저장
// 2)데이터 조회
// 3)데이터 수정
// 4)데이터 삭제
private void close(Connection con, Statement pstmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
private Connection getConnection() throws SQLException {
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
DataSourceUtils.getConnection()
DataSourceUtils.releaseConnection()
문제점
//트랜잭션 시작
try {
//비지니스 로직
//로직 정상 수행시 커밋
} catch (Exception e) {
//실패시 롤백
}
public class TransactionTemplate {
private PlatformTransactionManager transactionManager;
public <T> T execute(TransactionCallback<T> action){..}
void executeWithoutResult(Consumer<TransactionStatus> action){..}
}
execute() : 응답 값이 있을 때 사용
executeWithoutResult() : 응답 값이 없을 때 사용
템플릿 콜백 패턴을 적용하려면 템플릿을 제공하는 클래스를 작성해야 하는데, 스프링은 TransactionTemplate라는 템플릿 클래스를 제공한다.
@Slf4j
public class MemberServiceV3_2 {
private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult((status) -> {
try {
//비지니스 로직
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
//...
}
TransactionTemplate을 사용하기 위해서는 transactionManager가 필요하다.문제점

//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new
DefaultTransactionDefinition());
try {
//비즈니스 로직
bizLogic(fromId, toId, money);
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
프록시 도입 전 : 서비스에 비즈니스 로직과 트랜잭션 처리 로직이 함께 섞여있다.

//트랜잭션 프록시
public class TransactionProxy {
private MemberService target;
public void logic() {
//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(..);
try {
//실제 대상 호출
target.logic();
transactionManager.commit(status); //성공시 커밋
}
catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
}
//서비스
public class Service {
public void logic() {
//트랜잭션 관련 코드 제거, 순수 비즈니스 로직만 남음
bizLogic(fromId, toId, money);
}
}
프록시 도입 후 : 트랜잭션 프록시가 트랜잭션 처리 로직을 모두 가져가고 서비스 계층에는 순수한 비즈니즈 로직만 남길 수 있다.
스프링이 제공하는 AOP 기능을 사용하면 프록시를 매우 편리하게 적용할 수 있다.
-> 개발자는 트랜잭션 처리가 필요한 곳에 @Transactional 애노테이션만 붙여주면 된다. org.springframework.transaction.annotation.Transactional
@Slf4j
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
//...
}
@Transactional 추가하면서 순수한 비즈니스 로직은 남기고, 트랜잭션 관련 코드는 모두 제거했다.트랜잭션 AOP 정리

@Transactional 애노테이션만 추가하면 된다. 나머지는 스프링 트랜잭션 AOP가 자동으로 처리해준다.@Bean
DataSource dataSource() {
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
지금까지 데이터소스와 트랜잭션 매니저를 개발자가 직접 스프링 빈으로 등록해서 사용했다. 스프링을 통해 해당 부분이 자동화되었고 생략할 수 있다.
# application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
스프링 부트가 기본으로 생성하는 데이터소스는 커넥션풀을 제공하는 HikariDataSource이다. 커넥션풀과 관련된 설정도 application.properties를 통해서 지정할 수 있다.
스프링 부트는 적절한 트랜잭션 매니저를 자동으로 스프링 빈에 등록한다. 어떤 트랜잭션 매니저를 선택할지는 현재 등록된 라이브러리를 보고 판단하는데, JDBC를 기술을 사용하면 DataSourceTransactionManager를 빈으로 등록하고, JPA를 사용하면 JpaTransactionManager를 빈으로 등록한다.