-
스프링 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
'Data Base > 스프링 DB' 카테고리의 다른 글
스프링 DB - 트랜잭션 문제 해결(트랜잭션 템플릿) (1) 2023.11.28 스프링 DB - 트랜잭션 문제 해결(트랜잭션 매니저2) (0) 2023.11.28 스프링 DB - 트랜잭션 동기화 (1) 2023.11.27 스프링 DB - 트랜잭션 추상화 (0) 2023.11.27 스프링 DB - 스프링과 문제 해결(트랜잭션) (1) 2023.11.27