스프링이 제공하는 트랜잭션 매니저의 역할

  • 트랜잭션 추상화
  • 리소스 동기화

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

  • 트랜잭션 매니저는 내부적으로 트랜잭션 동기화 매니저를 사용

  • 트랜잭션 동기화 매니저는 쓰레드 로컬을 사용하여 커넥션을 동기화하기 때문에 멀티스레드 상황에서 안전하게 커넥션을 동기화 할 수 있음

  • 따라서, 커넥션이 필요하면 트랜잭션 동기화 매니저를 통해 커넥션을 획득

  • 동작 방식

    1. 트랜잭션 매니저는 데이터 소스를 통해 커넥션을 만들고 트랜잭션을 시작
    2. 트랜잭션 매니저는 트랜잭션이 시작된 커넥션을 트랜잭션 동기화 매니저에 보관
    3. 리포지토리는 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용
    4. 트랜잭션이 종료되면 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 커넥션을 통해 트랜잭션을 종료하고 커넥션을 닫음

스프링 트랜잭션 매니저를 적용한 애플리케이션 코드

MemberRepository

public class MemberRepositoryV3 {  

    private final DataSource dataSource;  

    public Member save(Member member) throws SQLException {  
        String sql = "insert into member (member_id, money) values (?, ?)";  

        Connection con = null;  
        PreparedStatement pstmt = null;  


        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, member.getMemberId());  
            pstmt.setInt(2, member.getMoney());  

            pstmt.executeUpdate();  

            return member;  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, null);  
        }  

    }  

    public Member findById(String memberId) throws SQLException {  
        String sql = "select * from member where member_id = ?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, memberId);  

            rs = pstmt.executeQuery();  
            if (rs.next()) {  
                Member member = new Member();  
                member.setMemberId(rs.getString("member_id"));  
                member.setMoney(rs.getInt("money"));  

                return member;  
            } else {  
                throw new NoSuchElementException("member not found memberId=" + memberId);  
            }  


        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  


    public void update(String memberId, int money) throws SQLException {  
        String sql = "update member set money=? where member_id=?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setInt(1, money);  
            pstmt.setString(2, memberId);  

            int resultSize = pstmt.executeUpdate();  
            log.info("result size = {}", resultSize);  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    public void delete(String memberId) throws SQLException {  
        String sql = "delete from member where member_id=?";  

        Connection con = null;  
        PreparedStatement pstmt = null;  
        ResultSet rs = null;  

        try {  
            con = getConnection();  

            pstmt = con.prepareStatement(sql);  
            pstmt.setString(1, memberId);  

            int resultSize = pstmt.executeUpdate();  
            log.info("result size = {}", resultSize);  
        } catch (SQLException e) {  
            log.error("db error", e);  
            throw e;  
        } finally {  
            close(con, pstmt, rs);  
        }  
    }  

    private Connection getConnection() throws SQLException {  
        // 트랜잭션 동기화를 사용하려면 DataSoruceUtils를 사용해야 함  
        Connection conn = DataSourceUtils.getConnection(dataSource);  
        log.info("conn = {}, conn.class = {}", conn, conn.getClass());  
        return conn;  
    }  

    private void close(Connection con, Statement stmt, ResultSet rs) throws SQLException {  
        JdbcUtils.closeResultSet(rs);  
        JdbcUtils.closeStatement(stmt);  
        // 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 함  
        DataSourceUtils.releaseConnection(con, dataSource);  
    }  
}
  • DataSourceUtils.getConnection()

    • 내부적으로 TransactionSynchronizationManager(트랜잭션 동기화 매니저)를 사용
    • 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환, 없으면 새로운 커넥션 생성하여 반환
  • DataSourceUtils.releaseConnection()

    • 내부적으로 TransactionSynchronizationManager(트랜잭션 동기화 매니저)를 사용
    • 트랜잭션 동기화 매니저를 통해 파라미터로 받은 커넥션이 현재 스레드에서 트랜잭션과 연결되어 있는지 확인한 후, 연결되어 있으면 재사용하기 위해 닫지 않고 연결되어 있지 않으면 커넥션을 닫거나 커넥션 풀에 반환

MemberService

public class MemberServiceV3_1 {  

//    private final DataSource dataSource;  
    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 {  
            businessLogic(fromId, toId, money);  
            transactionManager.commit(status);  

        } catch (Exception e) {  
            transactionManager.rollback(status);  
            throw new IllegalStateException(e);  
        }  
    }  

    private void businessLogic(String fromId, String toId, int money) throws SQLException {  
        Member fromMember = memberRepository.findById(fromId);  
        Member toMember = memberRepository.findById(toId);  

        memberRepository.update(fromId, fromMember.getMoney() - money);  
        validation(toMember);  
        memberRepository.update(toId, toMember.getMoney() + money);  
    }  

    private void release(Connection con) {  
        if (con != null) {  
            try {  
                con.setAutoCommit(true);  
                con.close();  
            } catch (Exception e) {  
                log.info("error", e);  
            }  
        }  
    }  

    private void validation(Member member) {  
        if (member.getMemberId().equals("ex")) {  
            throw new IllegalStateException("이체중 예외 발생");  
        }  
    }  
}
  • private final PlatformTransactionManager transactionManager

    • 트랜잭션 매니저를 주입받음
    • 현재 MemberRepository에서 JDBC 기술을 사용하고 있기 때문에 DataSourceTransactionManager 구현체를 주입 받아야 함
    • 만약 JPA로 변경되면 JpaTransactionManager 구현체를 주입 받으면 됨
  • transactionManager.getTransaction()

    • 트랜잭션 시작
    • 현재 트랜잭션의 상태 정보가 포함된 TransactionStatus status를 반환, 이후 트랜잭션을 커밋 또는 롤백할 때 필요함
  • new DefaultTransactionDefinition()

    • 트랜잭션과 관련된 옵션을 지정할 수 있음
  • transactionManager.commit(status)

    • 트랜잭션이 성공하면 이 로직을 호출하여 커밋
  • transactionManager.rollback(status)

    • 문제가 발생하면 이 로직을 호출하여 롤백

정리

트랜잭션 시작

  1. 서비스 계층에서 transactionManager.getTransaction()를 호출하여 트랜잭션 시작
  2. 트랜잭션 매니저는 내부에서 데이터소스를 사용하여 커넥션 생성
  3. 커넥션을 수동 커밋 모드로 변경하여 실제 데이터베이스 트랜잭션 시작
  4. 커넥션을 트랜잭션 동기화 매니저에 보관
  5. 트랜잭션 동기화 매니저는 쓰레드 로컬에 커넥션을 보관한다. 따라서 멀티 쓰레드 환경에서 안전하게 커넥션을 보관할 수 있음

로직 실행

  1. 서비스 계층은 비즈니스 로직을 실행하면서 리포지토리의 메서드를 호출한다. 이때 트랜잭션을 유지하기 위해 커넥션을 파라미터로 전달하지 않음
  2. 리포지토리 메서드들은 트DataSourceUtils.getConnection()를 사용하여 트랜잭션 동기화 매니저에 보관된 커넥션을 꺼내서 사용
  3. 획득한 커넥션을 사용하여 SQL을 실행

트랜잭션 종료

  1. 비즈니스 로직이 끝나면 커밋 또는 롤백하여 트랜잭션을 종료
  2. 트랜잭션을 종료하려면 동기화된 커넥션이 필요하기 때문에 트랜잭션 동기화 매니저를 통해 동기화된 커넥션을 획득
  3. 획득한 커넥션을 통해 데이터베이스에 트랜잭션을 커밋하거나 롤백
  4. 전체 리소스 정리
    • 트랜잭션 동기화 매니저를 정리
    • con.setAutoCommit(true)로 되돌림
    • con.close()를 통해 커넥션을 종료한다. 만약 커넥션 풀을 사용하는 경우라면 커넥션 풀에 반환

+ Recent posts