ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 DB - 트랜잭션 문제 해결(트랜잭션 매니저1)
    Data Base/스프링 DB 2023. 11. 28. 10:58

    트랜잭션 문제 해결(트랜잭션 매니저1)

     

    MemberRepositoryV3

    package hello.jdbc.repository;
    
    import hello.jdbc.domain.Member;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.jdbc.datasource.DataSourceUtils;
    import org.springframework.jdbc.support.JdbcUtils;
    
    import javax.sql.DataSource;
    import java.sql.*;
    import java.util.NoSuchElementException;
    
    /**
     * 트랜잭션 - 트랜잭션 매니저
     * DataSourceUtils.getConnection()
     * DataSourceUtils.releaseConnection()
     */
    @Slf4j
    public class MemberRepositoryV3 {
    
        private final DataSource dataSource;
    
        public MemberRepositoryV3(DataSource dataSource){
            this.dataSource = dataSource;
        }
    
        public Member save(Member member) throws SQLException{
            String sql = "insert into member(member_id, money) values(?, ?)";
    
            Connection con = null;
            //Statment - 그냥 sql
            //파라미터 바인딩 기능
            PreparedStatement pstmt = null;
    
            try {
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setString(1,member.getMemberId());
                pstmt.setInt(2,member.getMoney());
                //영향 받은 row의 숫자를 반환
                int count = pstmt.executeUpdate();
                return  member;
            }catch (SQLException e){
                log.error("db error",e);
                e.printStackTrace();
                throw  e;
            }finally {
                //Exception이 발생할 경우 finally가 실행x
                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);
    
                //DB에서 정보 조회시
                rs = pstmt.executeQuery();
                //DB로 반환받은 직후의 rs에서는 커서가 아무것도 가르키지 않는다.
                //rs.next를 해주어야 실제 데이터가 있는지 확인한뒤
                //데이터가 실제 시작하는 곳 앞으로 커서가 이동한다.
                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 memeberId="+memberId);
                }
    
    
            }catch (SQLException e){
                log.info("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;
    
            try {
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setInt(1,money);
                pstmt.setString(2,memberId);
                //영향 받은 row의 숫자를 반환
                int resultSize = pstmt.executeUpdate();
                log.info("resultSize={}",resultSize);
            }catch (SQLException e){
                log.error("db error",e);
                throw e;
            }finally {
                //Exception이 발생할 경우 finally가 실행x
                close(con,pstmt,null);
            }
    
        }
    
    
    
        public void delete(String memberId) throws SQLException {
            String sql = "delete from member where member_id= ?";
    
            Connection con = null;
            PreparedStatement pstmt = null;
    
            try{
                con = getConnection();
                pstmt = con.prepareStatement(sql);
                pstmt.setString(1,memberId);
                pstmt.executeUpdate();
            }catch (SQLException e){
                log.error("db error",e);
                throw e;
            }finally {
                //Exception이 발생할 경우 finally가 실행x
                close(con,pstmt,null);
            }
    
    
        }
        private void close(Connection con, Statement stmt, ResultSet rs){
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(stmt);
            //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtil를 사용해야 한다.
            DataSourceUtils.releaseConnection(con, dataSource);
        }
    
        private Connection getConnection() throws SQLException {
            //주의! 트랜잭션 동기화를 사용하려면 DatsSourceUtil을 사용해야한다.
            Connection con = DataSourceUtils.getConnection(dataSource);
            log.info("get connection = {}, class ={}",con, con.getClass());
            return con;
        }
    
    }
    • 커넥션을 파라미터로 전달하는 부분이 모두 제거 되었다.

     

    DataSourceUtil.getConnection()

    • getConnection()에서 DataSourceUtils.getConnection()를 사용하도록 변경된 부분을 특히 주의해야 한다.
    • DataSourceUtils.getConnection()는 다음과 같이 동작한다.
      • 트랜잭션 동기화 매니저가 관리한느 커넥션이 있으면 해당 커넥션을 반환한다.
      • 트랜잭션 동기화 매니저가 관리하는 컨넥션이 없는 경우 새로운 커넥션을 생성해서 반환한다.

     

    DataSourceUtils.releaseConnection()

    • close()에서 DataSourceUtils.releaseConnection()를 사용하도록 변경된 부분을 특히 주의해야 한다. 커넥션을 con.close()를 사용해서 직접 닫아버리면 커넥션이 유지되지 않는 문제가 발생한다. 이 커넥션은 이후 로직은 물론이고, 트랜잭션을 종료(커밋,롤백)할 때 까지살갈아있어야 한다.(서비스 로직에서의 일련의 과정이 아직 끝나지 않났을 수 있다.)
    • DataSourceUtils.releaseConnection()을 사용하면 커넥션을 바로 닫는 것이 아니다.
      • 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다.
      • 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다.   

     

    MemberServiceV3_1

    package hello.jdbc.service;
    
    
    import hello.jdbc.domain.Member;
    import hello.jdbc.repository.MemberRepositoryV2;
    import hello.jdbc.repository.MemberRepositoryV3;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.TransactionStatus;
    import org.springframework.transaction.support.DefaultTransactionDefinition;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.SQLException;
    
    /**
     * 트랜잭션 - 트랜잭션 매니저
     */
    @Slf4j
    @RequiredArgsConstructor
    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{
                //비지니스 로직
                bizLogic( fromId, toId, money);
                //성공시 커밋
                transactionManager.commit(status);
            }catch (Exception e){
                log.info("error",e);
                //실패시 롤백
                transactionManager.rollback(status);
                throw 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);
            validation(toMember);
            memberRepository.update(toId,toMember.getMoney() + money);
            //커밋, 롤백
        }
    
    
        private void validation(Member toMember) {
            if(toMember.getMemberId().equals("ex")){
                throw new IllegalStateException("이체중 예외 발생");
            }
        }
    }
    • `private final PlatformTransactionManager  transactionManager`
      • 트랜잭션 매니저를 주입받는다. 지금은 JDBC 기술을 사용하기 때문에 `DataSourceTransactionManager` 구현체를 주입 받아한다. 
      • 물론 JPA 같은 기술로 변경되면 JpaTransactionManager를 주입받으면 된다.
    • `transactionManager.getTransaction()`
      • 트랜잭션을 시작한다.
      • `TransactionStatus status`를 반환한다. 현재 트랙잭션의 상태 정보가 포함되어 있다. 이후 트랜잭션을 커밋, 롤백할 때 필요하다.
    • `new DefaultTransactionDefinition()`
      • 트랜잭션과 관련된 옵션을 지정할 수 있다.
    • `transactionManager.commit(status)`
      • 문제가 발생하면 이 로직을 호출해서 트랜잭션을 롤백하면 된다.

     

    초기화 코드 설명

    @BeforeEach
    void before(){
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        memberRepository = new MemberRepositoryV3(dataSource);
    
        PlatformTransactionManager transactionManager= new DataSourceTransactionManager(dataSource);
        memberService = new MemberServiceV3_1(transactionManager,memberRepository);
    }
    • `new DataSourceTransactionManager(dataSource)`
      • JDBC 기술을 사용하므로, JDBC용 트랜잭션 매니저(`DataSourceTranactionManager`)를 선택해서 서비스에 주입한다.
      • 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성하므로`DataSource`가 필요하다.

     

    테스트 해보면 모든 결과가 정상 동작하는 것을 확인할 수 있다. 당연히 롤백 기능도 잘동작한다.

     

     

    [출저 - 스프링 DB 1편 - 데이터 접근 핵심 원리, 김영한]

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1

     

    스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의

    백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔

    www.inflearn.com

     

    댓글

Designed by Tistory.