이제 본격적으로 애플리케이션 코드에 트랜잭션 매니저를 적용해보겠습니다.
/**
* 트랜잭션 - 트랜잭션 매니저
* 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 {
...
}
public Member findById(String memberId) throws SQLException {
...
}
public void update(String memberId, int money) throws SQLException {
...
}
public void delete(String memberId) throws SQLException {
...
}
//...2
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
DataSourceUtils.releaseConnection(con, dataSource);
}
//...1
private Connection getConnection() throws SQLException {
//주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={} class={}", con, con.getClass());
return con;
}
}
MemberRepositoryV2 와 다르게 커넥션을 파라미터로 전달하는 부분을 모두 제거했습니다. save(), findById(), update(), delete() 는 기존 코드와 동일합니다.
DataSourceUtils.getConnection()
MemberRepositoryV1 에서 getConnection() 를 사용했는데요, 이 부분을 DataSourceUtils.getConnection() 를 사용하도록 변경했습니다.
트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 하기 때문입니다.
내부를 들어가보면 TransactionSynchronizationManager 을 사용하고 있습니다. 트랜잭션 동기화 매니저에서 조회를 하는 것이죠.
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
...
DataSourceUtils.getConnection() 는 다음과 같이 동작합니다.
**트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환합니다.**
추가적으로 서비스 계층에서 트랜잭션 매니저를 안쓰는 경우가 있을 수 있습니다. 트랜잭션 없이 로직을 돌리고 싶은 경우가 있을 수 있겠죠.
이 때 커넥션이 없으면 문제가 되는데요, 이렇게 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 새로운 커넥션을 생성해서 반환합니다.
DataSourceUtils.releaseConnection()
MemberRepositoryV1 에서 JdbcUtils.closeConnection() 을 사용했습니다. 내부에서는 con.close() 를 사용해서 닫아주고 있죠.
public static void closeConnection(@Nullable Connection con) {
if (con != null) {
try {
con.close();
}
catch (SQLException ex) {
logger.debug("Could not close JDBC Connection", ex);
}
catch (Throwable ex) {
// We don't trust the JDBC driver: It might throw RuntimeException or Error.
logger.debug("Unexpected exception on closing JDBC Connection", ex);
}
}
}
커넥션을 con.close() 를 사용해서 직접 닫아버리면 커넥션이 유지되지 않는 문제가 발생합니다. (그래서 MemberRepositoryV2 에서도 파라미터로 커넥션을 넘길 때 커넥션을 닫지 않았습니다.)
이 커넥션은 이후 로직은 물론이고, 트랜잭션을 종료(커밋, 롤백)할 때 까지 살아있어야 합니다.
그래서 DataSourceUtils를 사용해야 합니다.
DataSourceUtils.releaseConnection() 을 사용하면 커넥션을 바로 닫는 것이 아닙니다.
트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해줍니다.
결국 서비스 계층에서 트랜잭션 매니저가 트랜잭션 동기화 매니저에 커넥션을 넣어줬기 때문에 서비스 계층에서 트랜잭션 매니저를 통해서 닫아줘야하기 때문이죠.
트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우는 서비스 계층에서 트랜잭션 매니저가 넣어준게 아니라는 뜻이기 때문에 바로 해당 커넥션을 닫습니다.
이제 트랜잭션 매니저를 사용하는 서비스 코드를 작성해봅시다.
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 java.sql.SQLException;
/**
* 트랜잭션 - 트랜잭션 매니저
*/
@RequiredArgsConstructor
@Slf4j
public class MemberServiceV3_1 {
//...1
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 memberRepository;
public void accountTransfer(String fromId, String toId, int money) {
//트랜잭션 시작 ...2,3
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
bizLogic(fromId, toId, money);
//...4
transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
//...5
transactionManager.rollback(status); //실패시 롤백
throw new IllegalStateException(e);
}
}
//...6
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("이체중 예외 발생");
}
}
}