- 이전 글의 트랜잭션 예시에서 여러 문제가 있었는데, 스프링은 이 문제들을 해결할 수 있는 다양한 방법과 기술들을 제공한다. 지금부터 스프링을 사용해서 이러한 문제들을 하나씩 해결해보자.
트랜잭션 추상화와 리소스 동기화(공유)
- 문제 1 : 서비스 계층이 트랜잭션을 사용하기 위해 JDBC 기술에 의존하던 문제
-> 해결 : 트랜잭션 추상화- 문제 2 : 리포지토리 계층에서 Connection을 인자로 받는 메소드와 받지 않는 메소드를 중복해서 만들어야 하던 문제
-> 해결 : 리소스 동기화(공유)
- 스프링은 트랜잭션 기능을 추상화하는 PlatformTransactionManager 인터페이스를 제공하며, 커넥션을 동기화(공유)해주는 TransactionSynchronizationManager 클래스를 제공한다.
- 해당 글에서는 PlatformTransactionManager 인터페이스와 구현체를 포함해서 “트랜잭션 매니저”, TransactionSynchronizationManager 클래스를 “트랜잭션 동기화 매니저”라고 이야기하겠다.
- 트랜잭션 매니저는 내부에서 트랜잭션 동기화 매니저를 사용한다.
- PlatformTransactionManager
package org.springframework.transaction; 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 인터페이스에 의존한다.
- 데이터 접근 기술에 따른 PlatformTransactionManager 인터페이스의 구현체는 대부분 만들어져 있다.
- 데이터 접근 기술에 따른 PlatformTransactionManager 인터페이스의 구현체는 대부분 만들어져 있다.
- 서비스 계층에서는 특정 트랜잭션 기술에 직접 의존하는 것이 아니라, PlatformTransactionManager 인터페이스에 의존한다.
- TransactionSynchronizationManager
package org.springframework.transaction.support; public abstract class TransactionSynchronizationManager { ... }
- 트랜잭션 동기화 매니저는 쓰레드 로컬(ThreadLocal)을 사용해서 커넥션을 동기화(공유)해준다. 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화할 수 있다.
- 쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여된다. 따라서 해당 쓰레드만 해당 데이터에 접근할 수 있다.
- 쓰레드 로컬을 사용하면 각각의 쓰레드마다 별도의 저장소가 부여된다. 따라서 해당 쓰레드만 해당 데이터에 접근할 수 있다.
- 트랜잭션 동기화 매니저는 쓰레드 로컬(ThreadLocal)을 사용해서 커넥션을 동기화(공유)해준다. 쓰레드 로컬을 사용하기 때문에 멀티쓰레드 상황에 안전하게 커넥션을 동기화할 수 있다.
- 트랜잭션 매니저와 트랜잭션 동기화 매니저의 동작 방식 - DataSourceTransactionManager 예시
1.서비스 계층에서 transactionManager.getTransaction()을 호출해서 트랜잭션을 시작한다.
2.트랜잭션 매니저는 내부에서 데이터소스를 사용해서 커넥션을 생성한다.
3.트랜잭션 매니저는 커넥션을 수동 커밋 모드로 변경해서 실제 데이터베이스 트랜잭션을 시작한다.
4.트랜잭션 매니저는 커넥션을 트랜잭션 동기화 매니저에 보관한다.
5.트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관한다.6.서비스는 비즈니스 로직을 실행하면서 리포지토리 계층의 메서드들을 호출한다.
7.리포지토리 계층에서 DataSourceUtils.getConnection()을 호출해서 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. (트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환)
8.획득한 커넥션을 사용해서 SQL을 데이터베이스에 전달해서 실행한다.9.비즈니스 로직이 끝난 후 transactionManager.commit()나 transactionManager.rollback()를 호출하여 트랜잭션을 종료한다.
10.트랜잭션을 종료하려면 동기화된 커넥션이 필요한데, 트랜잭션 매니저는 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득한다.
11.트랜잭션 매니저는 획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백한다.
12.트랜잭션 매니저는 전체 리소스를 정리한다. (트랜잭션 동기화 매니저 정리, 커넥션을 자동 커밋 모드로 변경한 후 커넥션 풀에 반환 등)
트랜잭션 매니저 예시
이전 글 참고
- MemberRepositoryV2 클래스 - JDBC 사용
@Repository public class MemberRepositoryV2 implements MemberRepository { private final DataSource dataSource; public MemberRepositoryV2(DataSource dataSource) { this.dataSource = dataSource; } private Connection getConnection() throws SQLException { //트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다. //트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다. //트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환한다. Connection con = DataSourceUtils.getConnection(dataSource); return con; } private void close(Connection con, Statement stmt, ResultSet rs) { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(stmt); //트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다. //트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다. //트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다. DataSourceUtils.releaseConnection(con, dataSource); } //save(Member member)... //findById(String memberId)... //update(String memberId, int money).... //delete(String memberId).... }
- MemberServiceV2 클래스
@Service public class MemberServiceV2 { private final PlatformTransactionManager transactionManager; private final MemberRepository memberRepository; public MemberServiceV2(PlatformTransactionManager transactionManager, MemberRepository memberRepository) { this.transactionManager = transactionManager; this.memberRepository = memberRepository; } public void accountTransfer(String fromId, String toId, int money) throws SQLException { //트랜잭션 시작 //TransactionStatus status를 반환 // 현재 트랜잭션의 상태 정보가 포함되어 있다. 이후 트랜잭션을 커밋, 롤백할 때 필요하다. 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 { Member fromMember = memberRepository.findById(fromId); Member toMember = memberRepository.findById(toId); memberRepository.update(fromId, fromMember.getMoney() - money); memberRepository.update(toId, toMember.getMoney() + money); } }
트랜잭션 템플릿
- 문제 3 : 서비스 계층에서 트랜잭션 적용 코드(트랜잭션 시작, 비즈니스 로직 실행, 성공 시 커밋, 실패 시 롤백)에 반복이 있던 문제
-> 해결 : 템플릿 콜백 패턴
- 스프링은 템플릿 콜백 패턴을 지원하는 TransactionTemplate 클래스를 제공한다.
- 템플릿 콜백 패턴이란 코드의 반복적인 구조와 변하지 않는 부분을 템플릿으로 만들고, 변화가 필요한 부분만을 콜백으로 분리하여 제공하는 설계 패턴이다.
- 템플릿 콜백 패턴이란 코드의 반복적인 구조와 변하지 않는 부분을 템플릿으로 만들고, 변화가 필요한 부분만을 콜백으로 분리하여 제공하는 설계 패턴이다.
- TransactionTemplate
package org.springframework.transaction.support; public class TransactionTemplate { private PlatformTransactionManager transactionManager; // 응답 값이 있을 때 사용 public <T> T execute(TransactionCallback<T> action) {..} // 응답 값이 없을 때 사용 void executeWithoutResult(Consumer<TransactionStatus> action) {..} ... }
트랜잭션 템플릿 예시
- MemberRepositoryV3 클래스
@Repository public class MemberRepositoryV3 implements MemberRepository { // MemberRepositoryV2와 동일 }
- MemberServiceV3 클래스
@Service public class MemberServiceV3 { private final TransactionTemplate txTemplate; private final MemberRepository memberRepository; // TransactionTemplate을 사용하려면 transactionManager가 필요하다. // 생성자에서 transactionManager를 주입 받으면서 TransactionTemplate을 생성 public MemberServiceV3(PlatformTransactionManager transactionManager, MemberRepository 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); } }); } private void bizLogic(String fromId, String toId, int money) throws SQLException { Member fromMember = memberRepository.findById(fromId); Member toMember = memberRepository.findById(toId); memberRepository.update(fromId, fromMember.getMoney() - money); memberRepository.update(toId, toMember.getMoney() + money); } }
- accountTransfer 메소드에서 예외를 처리하기 위해 try~catch가 들어갔는데, bizLogic() 메소드를 호출하면 SQLException 체크 예외를 넘겨준다. 해당 람다에서 체크 예외를 밖으로 던질 수 없기 때문에 언체크(런타임) 예외로 바꾸어 던지도록 예외를 전환했다.
참고 자료
“김영한 스프링 DB 1편”