Spring DB : 트랜잭션 동기화

    이 글은 인프런 김영한님의 강의를 수강하고 복습하며 작성한 글입니다.

    PlatformTransactionManager의 역할

    각 DB마다 트랜잭션 접근 방법이 다르기 때문에 트랜잭션 접근의 추상화가 필요했다. 스프링은 PlatformTransactionManager를 통해 이 부분을 추상화 해준다. 여기서 PlatformTransactionManager가 하는 일은 두 가지 일이 있다.

    1. 트랜잭션 추상화
    2. 리소스 동기화

    여기서 트랜잭션을 추상화 하는 방법은 앞선 게시글에서 정리가 되었다. 그렇다면 트랜잭션(리소스) 동기화는 무엇을 의미하는 것일까? 

     

    트랜잭션 동기화

    트랜잭션을 유지하려면 트랜잭션의 시작 ~ 끝까지 같은 DB 커넥션을 유지해야한다. 이전의 MemberServiceV2에서는 같은 Connection을 유지하기 위해 Service 계층에서 Connection을 만들고, 이것을 Repository 계층에 파라메터로 전달하며 문제를 해결했다. 

    커넥션을 유지하기 위해 커넥션을 파라미터로 전달하는 방법은 코드가 지저분해지고 변경 부분이 많아진다. 또한, 커넥션을 넘기는 메서드 / 넘기지 않는 메서드를 구별해서 만들어줘야한다. 따라서 커넥션을 파라메터로 넘겨서 트랜잭션(리소스) 동기화를 하는 방법은 권장되지 않는다. 

     

    트랜잭션 매니저 / 트랜잭션 동기화 매니저

    • 스프링은 트랜잭션 동기화 매니저를 제공한다. 트랜잭션 매니저는 내부에서 트랜잭션 동기화 매니저를 사용한다.
    • 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용한다. 따라서 멀티 쓰레드 상황에서 안전하게 커넥션을 동기화 할 수 있다. Repository 계층은 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 동기화 된 커넥션을 획득한다. ThreadLocal은 동일 쓰레드에서 사용되는 전역 변수 개념이기 때문에 이전처럼 파라미터로 커넥션을 전달하지 않아도 된다.

    트랜잭션 동기화 / 트랜잭션 매니저의 동작 방식을 간단히 설명하면 아래와 같다.

    1. 트랜잭션을 시작하려면 커넥션이 필요하다. 트랜잭션 매니저는 DataSource를 통해 Connection을 얻고, 트랜잭션을 시작한다.
    2. 트랜잭션 매니저는 트랜잭션이 시작된 Connection을 트랜잭션 동기화 매니저에게 보관한다.
    3. Repository는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용한다. 따라서 파라미터로 커넥션을 전달하지 않아도 된다. 
    4. 트랜잭션이 종료되는 시점에서 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해서 트랜잭션을 종료하고, 커넥션을 닫아준다.

     

     

    트랜잭션 매니저 사용 시, Connection 사용 / 회수

    DataSourceUtils.getConnection()

    • 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
    • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없으면 커넥션을 새로 생성해서 반환한다. 이런 경우는 서비스 계층에서 커넥션을 넘겨주지 않은 경우다. 보통 서비스 계층에서 커넥션을 얻어 트랜잭션을 시작하고, 이 커넥션을 트랜잭션 동기화 매니저에게 넘겨줘서 사용하게 된다. 만약 트랜잭션 동기화 매니저가 가진 커넥션이 없다면, 서비스 계층에서 커낵션이 생성되지 않은 것이다. 

     

    DataSourceUtils.releaseConnection()

    • Repository 계층에서 Connection 리소스 회수를 위한 방법은 JdbcUtils.closeConnect() → DataSourceUtils.releaseConnection()으로 변경됨. 
    • JdbcUtils.closeConnect()를 하면 Connection이 끝나거나 반납된다. 따라서 트랜잭션 때문에 커넥션을 사용해야하는 비즈니스 로직 계층에서 사용하지 못한다. 이 경우, Commit / RollBack을 사용할 수 없게 된다. 
    • DataSourceUtils.releaseConnection()을 사용하면 커넥션을 바로 닫지 않는다.
      • 트랜잭션을 사용하기 위해 동기화 된 커넥션은 커넥션을 닫지 않고 그대로 유지한다. 트랜잭션 동기화 매니저에서 언제든지 커넥션을 가져올 수 있는 상황이다.
      • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는(아닌) 경우 해당 커넥션을 닫는다. 

     

     

    트랜잭션 매니저를 이용한 트랜잭션 추상화

    MemberServiceV3_1 @ TransactionManager를 통한 트랜잭션 추상화

    @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 {
    
            // 트랜잭션 시작
            TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    
            try {
                // 핵심 로직
                bizLogic(fromId, toId, money);
                transactionManager.commit(status);
    
            } catch (Exception e) {
                System.out.println("here");
                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);
            validate(toMember);
            memberRepository.update(toId, toMember.getMoney() + money);
        }
    
        private void validate(Member toMember) {
            if (toMember.getMemberId().equals("ex")) {
                throw new IllegalStateException("이체 중 예외 발생");
            }
        }
    }

    TransactionManager를 도입해서 트랜잭션 추상화를 처리했다. 주요한 변화는 다음과 같다.

     

    // 트랜잭션 매니저 필드 선언
    private final PlatformTransactionManager transactionManager;
    
    // 실제 DI 위한 코드
    DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
    • TransactionManager를 DI받는다.
      • 어떤 DataSource를 사용하냐에 따라 다른 TransactionManager를 주입받는다. 주로 DataSourceTransactionManager를 주입 받는다.
      • TranscationManager를 생성할 때, DataSource가 필요하다
        • Transaction을 시작하기 위해서는 Connection이 필요하다. DataSource는 Connection을 얻어오는 방법이 추상화되었다. 따라서 TransactionManager에게 DataSource를 전달해줘야 정상적으로 Connection을 가진다.
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    • 트랜잭션을 시작한다.
    • 트랜잭션을 시작하면 다음 세 가지 일이 발생된다.
      • Connection을 얻어온다.
      • Connection에 set autoCommit False를 설정한다.
      • Connection을 트랜잭션 동기화 매니저에 넣는다.
    • 트랜잭션을 시작하면 TransactionStatus status가 반환된다.
      • TransactionStatus에는 현재 트랜잭션의 상태 정보가 포함되어있다.
      • Commit / RollBack에 필요하다. 
    • new DefaultTransactionDefinition()을 이용해 트랜잭션과 관련된 옵션을 지정할 수 있다. 
    transactionManager.commit(status);
    • TranscationManager는 commit을 통해 현재 트랜잭션을 커밋할 수 있다.
      • 이 때 현재 트랜잭션의 상태를 전달해야한다. 
    • Transcation을 Commit하면 발생하는 일
      • 실제로 Commit이 된다.
      • Connection이 반납된다. 리소스가 릴리즈 됨 + 트랜잭션 동기화 매니저에서 커넥션 삭제 + 커넥션 풀에 반납
    transactionManager.rollback(status);
    • 트랜잭션에 문제가 발생하면 Rollback을 호출해 트랜잭션을 처음으로 돌릴 수 있다.
      • 이 때, 현재 트랜잭션의 상태를 전달해야한다.

     

     

    MemberRepositoryV3 @ TransactionManager를 통한 트랜잭션 추상화

    @Slf4j
    public class MemberRepositoryV3 {
    
        private final DataSource dataSource;
    
        public MemberRepositoryV3(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
    	public void delete(String memberId) throws SQLException {
            String sql = "delete from member where member_id=?";
    
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
    
            try {
                // 트랜잭션 매니저 사용
                conn = getConnection();
                pstmt = conn.prepareStatement(sql);
    
                pstmt.setString(1, memberId);
    
                int resultSize = pstmt.executeUpdate();
                log.info("resultSize = {}", resultSize);
                log.info("Connection = {}, class = {}", conn,conn.getClass());
            } catch (SQLException e) {
                log.error("error", e);
                throw e;
            }finally {
                close(conn, pstmt, rs);
            }
    
        }
    
        // 새로 추가된 코드
        private Connection getConnection() {
            Connection connection = DataSourceUtils.getConnection(dataSource);
            log.info("get connection={}, class = {}", connection, connection.getClass());
            return connection;
        }
    
        private void close(Connection connection, PreparedStatement pstmt, ResultSet rs) {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
    //        JdbcUtils.closeConnection(connection);
            DataSourceUtils.releaseConnection(connection, dataSource);
            
    }

    TransactionManager를 이용한 트랜잭션 추상화를 위해 다음과 같이 코드가 수정되었다. 

    private Connection getConnection() {
        Connection connection = DataSourceUtils.getConnection(dataSource);
        log.info("get connection={}, class = {}", connection, connection.getClass());
        return connection;
    }
    • DataSourceUtils.getConnection(dataSource)
      • 위 메서드를 이용하면 현재 트랜잭션 매니저 내부의 트랜잭션 동기화 매니저에 저장된 Connection을 가져온다.
      • 트랜잭션 동기화 매니저는 트랜잭션 내의 Connection을 동기화해주는 역할을 한다. 
      • 이 때, dataSource를 넣는 이유는 특정 DataSource에 대해 트랜잭션 동기화 매니저가 커넥션을 가지고 있기 때문이다.
    private void close(Connection connection, PreparedStatement pstmt, ResultSet rs) {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
    //        JdbcUtils.closeConnection(connection);
            DataSourceUtils.releaseConnection(connection, dataSource);
        }
    • DataSourceUtils.releaseConnection(connection, dataSource)
      • releaseConnection()을 하면 트랜잭션 동기화 매니저에서 받은 트랜잭션이면, 트랜잭션 동기화 매니저에게 다시 동기화된 트랜잭션을 반납해준다. 이 때, 트랜잭션이 종료된 것은 아니다.
      • 여기서 트랜잭션을 종료하지 않는 이유는 트랜잭션은 비즈니스 로직이 시작 / 종료되는 서비스 계층에서 처리가 되어야 하기 때문이다. 

     

    MemberServiceV3Test_1 @ 트랜잭션 매니저로 트랜잭션 추상화

    @Slf4j
    @SpringBootTest
    class MemberServiceV3Test_1 {
    
        public static final String MEMBER_A = "memberA";
        public static final String MEMBER_B = "memberB";
        public static final String MEMBER_EX = "ex";
    
        private MemberRepositoryV3 memberRepository;
        private MemberServiceV3_1 memberService;
    
        @BeforeEach
        void beforeEach() {
    
            DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
            PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
            memberRepository = new MemberRepositoryV3(dataSource);
            memberService = new MemberServiceV3_1(transactionManager, memberRepository);
        }
        
        ...
    }
    • 테스트 코드 자체는 MemberServiceV2와 동일하다.
    • 트랜잭션 매니저를 DI 해주는 부분만 바뀌게 된다. 트랜잭션 매니저는 Connection을 얻어와서, 트랜잭션을 시작해주고 Connection을 동기화하는 기능을 제공한다. 
      • Connection은 DataSource에서 얻기 때문에 DataSourceTransactionManager를 만들 때, 반드시 DataSource를 전달해준다. 

     

    동작방식 그림 정리

    • 클라이언트 요청으로 서비스 계층에서 비즈니스 로직이 실행된다.
    • 비즈니스 로직이 시작될 때, TranscationManager.getransaction()을 한다.
    • getTransaction()을 하면, TransactionManager는 넘겨받은 DataSource에서 Connection을 얻는다. 그리고 수동 코밋 모드를 설정하고, 이 Connection을 트랜잭션 동기화 매니저에 저장한다. 트랜잭션 동기화 매니저는 Thread Local로 구현되어 있기 때문에 동일 쓰레드 내에서 공유한다.

    • 서비스 계층은 비즈니스 로직 내에서 Repository의 여러 메서드들을 호출한다. Repository의 각 메서드들은 DataSourceUtils.getConnection()을 통해 트랜잭션 동기화 매니저에 있는 Connection을 가져와 처리한다. 트랜잭션이 동기화 처리가 된다.

    • 비즈니스 로직이 끝나면 트랜잭션을 종료해야한다. 이 때 transcationManager.commit 혹은 transactionManager.rollBack()을 이용해 트랜잭션을 종료할 수 있다.
      • 위 메서드는 트랜잭션을 종료시켜준다. 트랜잭션을 종료하려면 동기화 된 Connection이 필요하다. 이 때, 트랜잭션 동기화 매니저를 통해 Connection을 획득한다. 
    • 얻은 Connection을 이용해 트랜잭션을 커밋하거나 롤백한다. 
      • 전체 리소스를 정리한다.
      • 트랜잭션 동기화 매니저를 정리한다. 트랜잭션 동기화 매니저는 쓰레드 로컬 기반이고, 사용 후 반드시 정리가 되어야한다.
      • Connection을 자동 커밋 모드 상태로 바꾼 후 커넥션 풀로 반납한다.

     

    트랜잭션 매니저 정리

    • Jdbc / JPA 기술은 각각 트랜잭션을 시작하는 방법이 달랐다. 따라서 트랜잭션의 추상화가 필요했다.
    • PlatformTransactionManager를 이용해 트랜잭션을 추상화 할 수 있다. TranscationManager는 내부적으로 트랜잭션 동기화 매니저(쓰레드 로컬 개념)을 가지고 있어, Connection을 동기화 해준다.
    • Repository 계층에서 트랜잭션 동기화 매니저에 있는 Connection을 DataSourceUtils.getConnection()으로 가져오고, 다시 쓰레드 로컬로 반환할 때는 DataSourceUtils.releaseConnection()을 사용했다. 이 때, JdbcUtils.closeConnection()은 사용하지 않는다.
    • 트랜잭션의 실제 종료 지점은 서비스 계층이 된다. 왜냐하면 비즈니스 로직이 수행되는 계층에서 트랜잭션이 시작되고 종료되기 때문이다. 
    • TransactionManager를 이용한 트랜잭션 추상화 덕분에 서비스 계층은 이제 JDBC 기술에 의존하지 않는다. 
      • 이후 JDBC에서 JPA로 변경해도 서비스 코드를 유지할 수 있다.
      • 기술 변경 시, DataSource의 의존관계 주입만 DataSourceTransactionManager에서 JpaTranscationManager로 변경해주면 된다. 
      • 트랜잭션 동기화 매니저 덕분에 커넥션을 파라미터로 넘기지 않아도 된다.

    댓글

    Designed by JB FACTORY