MemberRepositoryV2를 복사해서 MemberRepositoryV3를 만들자.
커넥션을 가져오는 부분을 아래와 같이 변경해준다
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
// 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
private Connection getConnection() throws SQLException {
// 주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
그리고 아래 부분은 지워준다.
public Member findById(Connection con, String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
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 {
// connection은 여기서 닫지 않는다.
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
}
}
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.error("db error", e);
throw e;
} finally {
// connection은 여기서 닫지 않는다.
JdbcUtils.closeStatement(pstmt);
}
}
- 커넥션을 파라미터로 전달하는 부분이 모두 제거
- DataSourceUtils.getConnection()
- getConnection() 에서 DataSourceUtils.getConnection() 를 사용하도록 변경된 부분을 특히
주의해야 한다. - DataSourceUtils.getConnection() 는 다음과 같이 동작한다
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환한다.
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환
DataSourceUtils.releaseConnection()
- close() 에서 DataSourceUtils.releaseConnection() 를 사용하도록 변경된 부분을 특히 주의해야
한다. 커넥션을 con.close() 를 사용해서 직접 닫아버리면 커넥션이 유지되지 않는 문제가 발생한다. 이
커넥션은 이후 로직은 물론이고, 트랜잭션을 종료(커밋, 롤백)할 때 까지 살아있어야 한다.
- DataSourceUtils.releaseConnection() 을 사용하면 커넥션을 바로 닫는 것이 아니다.
트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다.
트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다.
이제 트랜잭션 매니저를 사용하는 서비스를 작성해보자.
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 PlatformTransactionManager transactionManager;
// MemberRepositoryV3를 꼭 사용해주어야 한다.
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
- 트랜잭션 매니저를 주입 받는다. 지금은 JDBC 기술을 사용하기 때문에
DataSourceTransactionManager 구현체를 주입 받아야 한다.
- 물론 JPA 같은 기술로 변경되면 JpaTransactionManager 를 주입 받으면 된다.
- transactionManager.getTransaction()
트랜잭션을 시작한다.
- TransactionStatus status 를 반환한다. 현재 트랜잭션의 상태 정보가 포함되어 있다. 이후
트랜잭션을 커밋, 롤백할 때 필요하다.
- new DefaultTransactionDefinition()
트랜잭션과 관련된 옵션을 지정할 수 있다. 자세한 내용은 뒤에서 설명한다.
- transactionManager.commit(status)
트랜잭션이 성공하면 이 로직을 호출해서 커밋하면 된다.
- transactionManager.rollback(status)
문제가 발생하면 이 로직을 호출해서 트랜잭션을 롤백하면 된다.
테스트 코드를 작성해보자.
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV1;
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);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager();
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(8000);
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
}
돌리면 에러가 난다. 에러로그를 확인해보자.
아래 부분에서 파라미터로 dataSource를 넘겨주어야 한다.
@BeforeEach
void before() {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
memberRepository = new MemberRepositoryV3(dataSource);
// 파라미터로 dataSource를 넘겨주어야 한다.
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
memberService = new MemberServiceV3_1(transactionManager, memberRepository);
}
- new DataSourceTransactionManager(dataSource)
- JDBC 기술을 사용하므로, JDBC용 트랜잭션 매니저를 선택해서 서비스에 주입.
- 트랜잭션 매니저는 데이터소스를 통해 커넥션을 생성하므로 DataSource가 필요하다.
- 테스트 해보면 결과가 정상 동작하는 것을 확인 => 롤백 기능도 동작.
<출처 김영한: 스프링 DB 1편 - 데이터 접근 핵심 원리>
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의
백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., - 강의
www.inflearn.com
'Spring > SpringDB' 카테고리의 다른 글
트랜잭션 AOP 이해 (0) | 2022.06.20 |
---|---|
트랜잭션 문제 해결 - 트랜잭션 매니저2 & 트랜잭션 탬플릿 (0) | 2022.06.20 |
트랜잭션 추상화 & 트랜잭션 동기화 (0) | 2022.06.20 |
트랜잭션 적용 (0) | 2022.06.15 |
DB 락 - 변경 / 조회 (0) | 2022.06.15 |