일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링
- 인터페이스
- equals()
- ocp
- 오버라이딩
- @configuration
- 코드트리
- 싱글톤
- html form
- 코딩테스트
- DI
- 코드트리조별과제
- fielderror
- 의존관계
- 테스트코드
- java
- objecterror
- 오블완
- 김영한
- 추상클래스
- 프록시
- http 메시지 컨버터
- 서블릿
- 스프링컨테이너
- 티스토리챌린지
- 다형성
- JSON
- HttpServletResponse
- 백준
- 참조변수
- Today
- Total
minOS
트랜잭션 문제 해결 - 트랜잭션 매니저1 본문
MemberRepositoryV2 vs MemberRepositoryV3
MemberRepositoryV2 - Connection 파라미터로 넘김
package hello.jdbc.repository; import hello.jdbc.domain.Member; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.support.JdbcUtils; import javax.sql.DataSource; import java.sql.*; import java.util.NoSuchElementException; /* JDBC - Connection을 파라미터로 넘기는 예제 */ @Slf4j public class MemberRepositoryV2 { private final DataSource dataSource; public MemberRepositoryV2(DataSource dataSource) { this.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); //SQL 전달 pstmt.setString(1, member.getMemberId()); //바라미터 바인딩 pstmt.setInt(2, member.getMoney()); pstmt.executeUpdate(); return member; } catch (SQLException e) { log.info("db error", e); throw e; } finally { close(con, pstmt, null); // 리소스 정리 } } public Member findById(Connection con ,String memberId) throws SQLException { String sql = "select * from member where member_id = ?"; PreparedStatement pstmt = null; ResultSet rs = null; try { 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 { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(pstmt); // JdbcUtils.closeConnection(con); 커넥션은 여기서 닫지 않음 왜냐면 서비스에서 종료해야함 } } 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(Connection con, String memberId, int money) throws SQLException { String sql = "update member set money=? where member_id=?"; PreparedStatement pstmt = null; try { pstmt = con.prepareStatement(sql); pstmt.setInt(1, money); pstmt.setString(2,memberId); int resultSize = pstmt.executeUpdate(); log.info("resultSize ={}",resultSize); } catch (SQLException e) { log.info("db error", e); throw e; } finally { JdbcUtils.closeStatement(pstmt); // 리소스 정리 // JdbcUtils.closeConnection(con); 커넥션은 여기서 닫지 않음 왜냐면 서비스에서 종료해야함 } } 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); int resultSize = pstmt.executeUpdate(); log.info("resultSize ={}",resultSize); } catch (SQLException e) { log.info("db error", e); throw e; } finally { 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.info("db error", e); throw e; } finally { close(con, pstmt, null); // 리소스 정리 } } private void close(Connection con, Statement stmt, ResultSet rs) { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(stmt); JdbcUtils.closeConnection(con); } private Connection getConnection() throws SQLException { Connection con = dataSource.getConnection(); log.info("get connection = {}, class = {}", con , con.getClass()); return con; } }
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; PreparedStatement pstmt = null; try { con = getConnection(); //커넥션 획득 pstmt = con.prepareStatement(sql); //SQL 전달 pstmt.setString(1, member.getMemberId()); //바라미터 바인딩 pstmt.setInt(2, member.getMoney()); pstmt.executeUpdate(); return member; } catch (SQLException e) { log.info("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; try { con = getConnection(); pstmt = con.prepareStatement(sql); pstmt.setInt(1, money); pstmt.setString(2,memberId); int resultSize = pstmt.executeUpdate(); log.info("resultSize ={}",resultSize); } catch (SQLException e) { log.info("db error", e); throw e; } finally { 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.info("db error", e); throw e; } finally { close(con, pstmt, null); // 리소스 정리 } } private void close(Connection con, Statement stmt, ResultSet rs) { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(stmt); //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다. DataSourceUtils.releaseConnection(con,dataSource); /** * 트랜잭션이 활성화된 경우) * 트랜잭션 동기화 매니저(TransactionSynchronizationManager)가 현재 트랜잭션 컨텍스트와 연결된 커넥션을 관리. * 이 경우, releaseConnection()은 커넥션을 닫지 않고 트랜잭션이 끝날 때까지 유지. * 트랜잭션이 활성화 x ) * 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우, 커넥션을 닫음 * */ //JdbcUtils.closeConnection(con); } private Connection getConnection() throws SQLException { // 주의 ! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다 Connection con = DataSourceUtils.getConnection(dataSource); // 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환 // 만약 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환 log.info("get connection = {}, class = {}", con , con.getClass()); return con; } }
- 커넥션을 파라미터로 전달하는 부분이 모두 제거되었다.
DataSourceUtils.getConnection()
- getConnection()` 에서 `DataSourceUtils.getConnection()` 를 사용하도록 변경된 부분을 특히 주의해야 한다.
- DataSourceUtils.getConnection()` 는 다음과 같이 동작한다.
1) 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
2) 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환한다.
DataSourceUtils.releaseConnection()
- close()에서 DataSourceUtils.releaseConnection()를 사용하도록 변경된 부분을 특히 주의해야한다. 커넥션을 con.close()를 사용해서 직접 닫아버리면 커넥션이 유지되지 않는 문제가 발생한다. 이 커넥션은 이후 로직은 물론이고, 트랜잭션을 종료(커밋, 롤백)할 때 까지 살아있어야 한다.
- DataSourceUtils.releaseConnection()을 사용하면 커넥션을 바로 닫는 것이 아니다.
1) 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다.
2) 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다
MemberServiceV3_1
package hello.jdbc.service; import hello.jdbc.domain.Member; 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){ 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); 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
1)트랜잭션 매니저를 주입 받는다. 지금은 JDBC 기술을 사용하기 때문에 DataSourceTransactionManager` 구현체를 주입 받아야 한다
2)물론 JPA 같은 기술로 변경되면 `JpaTransactionManager` 를 주입 받으면 된다.
- transactionManager.getTransaction()1)트랜잭션을 시작한다.
2) TransactionStatus status 를 반환한다. 현재 트랜잭션의 상태 정보가 포함되어 있다. 이후 트랜잭션을 커밋, 롤백할 때 필요하다.
- new DefaultTransactionDefinition()
1) 트랜잭션과 관련된 옵션을 지정할 수 있다. 자세한 내용은 뒤에서 설명한다.
- transactionManager.commit(status)
1)트랜잭션이 성공하면 이 로직을 호출해서 커밋하면 된다.
- transactionManager.rollback(status)
1) 문제가 발생하면 이 로직을 호출해서 트랜잭션을 롤백하면 된다.
MemberServiceV3_1Test
package hello.jdbc.service; import hello.jdbc.domain.Member; import hello.jdbc.repository.MemberRepositoryV2; import hello.jdbc.repository.MemberRepositoryV3; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.transaction.PlatformTransactionManager; import java.sql.SQLException; import static hello.jdbc.connection.ConnectionConst.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * 트랜잭션 - 트랜잭션 매니저 * */ class MemberServiceV3_1Test { 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 before(){ DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD); memberRepository = new MemberRepositoryV3(dataSource); // JDBC 기반 트랜잭션 관리 구현체 주입 PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); memberService = new MemberServiceV3_1(transactionManager,memberRepository); } @AfterEach void after() throws SQLException{ memberRepository.delete(MEMBER_A); memberRepository.delete(MEMBER_B); memberRepository.delete(MEMBER_EX); } @Test @DisplayName("정상 이체") void accountTransfer() throws SQLException { //given Member memberA = new Member(MEMBER_A, 10000); Member memberB = new Member(MEMBER_B, 10000); memberRepository.save(memberA); memberRepository.save(memberB); //when memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000); //then Member findMemberA = memberRepository.findById(memberA.getMemberId()); Member findMemberB = memberRepository.findById(memberB.getMemberId()); assertThat(findMemberA.getMoney()).isEqualTo(8000); assertThat(findMemberB.getMoney()).isEqualTo(12000); } @Test @DisplayName("이체 중 예외 발생") void accountTransferEx() throws SQLException { //given Member memberA = new Member(MEMBER_A, 10000); Member memberEx = new Member(MEMBER_EX, 10000); memberRepository.save(memberA); memberRepository.save(memberEx); //when assertThatThrownBy(() ->memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000)) .isInstanceOf(IllegalStateException.class); //then Member findMemberA = memberRepository.findById(memberA.getMemberId()); Member findMemberB = memberRepository.findById(memberEx.getMemberId()); assertThat(findMemberA.getMoney()).isEqualTo(10000); //A 에서 2000원 빠진 후 예외 터짐 -> 롤백 되기 때문에 비지니스 로직 실행 전으로 돌아감 assertThat(findMemberB.getMoney()).isEqualTo(10000); } }
초기화 코드 설명
@BeforeEach void before(){ DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD); memberRepository = new MemberRepositoryV3(dataSource); // JDBC 기반 트랜잭션 관리 구현체 주입 PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); memberService = new MemberServiceV3_1(transactionManager,memberRepository); }
- new DataSourceTransactionManager(dataSource)
1) JDBC 기술을 사용하므로, JDBC용 트랜잭션 매니저(`DataSourceTransactionManager` )를 선택해서 서비스에 주입한다.
2) 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성하므로 `DataSource` 가 필요하다.
'TIL > 김영한의 스프링 DB 1편' 카테고리의 다른 글
트랜잭션 문제 해결 - 트랜잭션 템플릿 (0) | 2024.12.07 |
---|---|
트랜잭션 문제 해결 - 트랜잭션 매니저2 (0) | 2024.12.07 |
스프링과 문제 해결 - 트랜잭션 동기화 (0) | 2024.11.29 |
스프링과 문제 해결 - 트랜잭션 추상화 (0) | 2024.11.28 |
스프링과 문제 해결 - 문제점들 (1) | 2024.11.28 |