μ¬κΈ°μ κ°μ₯ μ€μν κ³³μ, λ°λ‘ ν΅μ¬ λΉμ¦λμ€ λ‘μ§μ΄ λ€μ΄μλ μλΉμ€ κ³μΈ΅μ΄λ€!!
μ§κΈκΉμ§ μ°λ¦¬κ° κ°λ°ν μ ν리μΌμ΄μ μ λ¬Έμ μ μ ν¬κ² 3κ°μ§μ΄λ€.
try
, catch
, finally
...SQLException
μ μ²΄ν¬ μμΈμ΄κΈ° λλ¬Έμ λ°μ΄ν° μ κ·Ό κ³μΈ΅μ νΈμΆν μλΉμ€ κ³μΈ΅μμ ν΄λΉ μμΈλ₯Ό μ‘μμ μ²λ¦¬νκ±°λ λͺ
μμ μΌλ‘ throws
λ₯Ό ν΅ν΄μ λ€μ λ°μΌλ‘ λμ ΈμΌνλ€.SQLException
μ JDBC μ μ© κΈ°μ μ΄λ€. ν₯ν JPAλ λ€λ₯Έ λ°μ΄ν° μ κ·Ό κΈ°μ μ μ¬μ©νλ©΄, κ·Έμ λ§λ λ€λ₯Έ μμΈλ‘ λ³κ²½ν΄μΌ νκ³ , κ²°κ΅ μλΉμ€ μ½λλ μμ ν΄μΌ νλ€.MemberRepository
μ½λλ μμν JDBCλ₯Ό μ¬μ©νλ€.try
, catch
, finally
...PreparedStatement
λ₯Ό μ¬μ©νκ³ , κ²°κ³Όλ₯Ό 맀ννκ³ ... μ€ννκ³ , 컀λ₯μ
κ³Ό 리μμ€λ₯Ό μ 리νλ€.νμ¬ μλΉμ€ κ³μΈ΅μ νΈλμμ μ μ¬μ©νκΈ° μν΄μ JDBC κΈ°μ μ μμ‘΄νκ³ μλ€. ν₯ν JDBCμμ JPA κ°μ λ€λ₯Έ λ°μ΄ν° μ κ·Ό κΈ°μ λ‘ λ³κ²½νλ©΄, μλΉμ€ κ³μΈ΅μ νΈλμμ κ΄λ ¨ μ½λλ λͺ¨λ ν¨κ» μμ ν΄μΌ νλ€.
JDBC νΈλμμ
μμ‘΄
JPA κΈ°μ λ‘ λ³κ²½
μ΄ λ¬Έμ λ₯Ό ν΄κ²°νλ €λ©΄ νΈλμμ
κΈ°λ₯μ μΆμννλ©΄ λλ€.
λ¨μνκ² μκ°νμ λ νΈλμμ
μΆμν μΈν°νμ΄μ€(TxManager)λ₯Ό λ§λ€μ΄μ μ¬μ©νλ©΄ λλ€.
κ·Έλ¦¬κ³ μλ κ·Έλ¦Όκ³Ό κ°μ΄ TxManager
μΈν°νμ΄μ€λ₯Ό κΈ°λ°μΌλ‘ κ°κ°μ κΈ°μ μ λ§λ ꡬν체λ₯Ό λ§λ€λ©΄ λλ€.
TxManager
λΌλ μΆμνλ μΈν°νμ΄μ€μ μμ‘΄νλ€. μ΄μ μνλ ꡬν체λ₯Ό DIλ₯Ό ν΅ν΄μ μ£Όμ
νλ©΄ λλ€. μλ₯Ό λ€μ΄μ JDBC νΈλμμ
κΈ°λ₯μ΄ νμνλ©΄JdbcTxManager
λ₯Ό μλΉμ€μ μ£Όμ
νκ³ , JPA νΈλμμ
κΈ°λ₯μΌλ‘ λ³κ²½ν΄μΌ νλ©΄ JpaTxManager
λ₯Ό μ£Όμ
νλ©΄ λλ€.PlatformTransactionManager
μΈν°νμ΄μ€μ΄λ€. μ½λλ₯Ό μ΄ν΄λ³΄λ©΄ λ€μκ³Ό κ°λ€.package org.springframework.transaction;
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
getTransaction()
: νΈλμμ
μ μμνλ€.commit()
: νΈλμμ
μ 컀λ°νλ€.rollback()
: νΈλμμ
μ λ‘€λ°±νλ€.λ€μμλ PlatformTransactionManager
μΈν°νμ΄μ€μ ꡬν체λ₯Ό ν¬ν¨ν΄μ νΈλμμ
맀λμ λ‘ μ€μ¬μ μ΄μΌκΈ°νλ€.
μ€νλ§μ΄ μ 곡νλ νΈλμμ 맀λμ λ ν¬κ² 2κ°μ§ μν μ νλ€.
μ€νλ§μ΄ μ 곡νλ νΈλμμ
λκΈ°ν 맀λμ λ, μ°λ λ λ‘컬( ThreadLocal
)μ μ¬μ©ν΄μ 컀λ₯μ
μ λκΈ°ννλ€. νΈλμμ
맀λμ λ λ΄λΆμμ μ΄ νΈλμμ
λκΈ°ν 맀λμ λ₯Ό μ¬μ©νλ€.
νΈλμμ
λκΈ°ν 맀λμ λ μ°λ λ λ‘컬μ μ¬μ©νκΈ° λλ¬Έμ λ©ν°μ°λ λ μν©μ μμ νκ² μ»€λ₯μ
μ λκΈ°ν ν μ μλ€. λ°λΌμ 컀λ₯μ
μ΄ νμνλ©΄ νΈλμμ
λκΈ°ν 맀λμ λ₯Ό ν΅ν΄ 컀λ₯μ
μ νλνλ©΄ λλ€. λ°λΌμ μ΄μ μ²λΌ νλΌλ―Έν°λ‘ 컀λ₯μ
μ μ λ¬νμ§ μμλ λλ€.
λμ λ°©μ
1. νΈλμμ
μ μμνλ €λ©΄ 컀λ₯μ
μ΄ νμνλ€. νΈλμμ
맀λμ λ λ°μ΄ν°μμ€λ₯Ό ν΅ν΄ 컀λ₯μ
μ λ§λ€κ³ νΈλμμ
μ μμνλ€.
2. νΈλμμ
맀λμ λ νΈλμμ
μ΄ μμλ 컀λ₯μ
μ νΈλμμ
λκΈ°ν 맀λμ μ 보κ΄νλ€.
3. 리ν¬μ§ν 리λ νΈλμμ
λκΈ°ν 맀λμ μ 보κ΄λ 컀λ₯μ
μ κΊΌλ΄μ μ¬μ©νλ€. λ°λΌμ νλΌλ―Έν°λ‘ 컀λ₯μ
μ μ λ¬νμ§ μμλ λλ€.
4. νΈλμμ
μ΄ μ’
λ£λλ©΄ νΈλμμ
맀λμ λ νΈλμμ
λκΈ°ν 맀λμ μ 보κ΄λ 컀λ₯μ
μ ν΅ν΄ νΈλμμ
μ μ’
λ£νκ³ , 컀λ₯μ
λ λ«λλ€.
κΈ°μ‘΄ μ΄ν리μΌμ΄μ μ½λμ νΈλμμ 맀λμ λ₯Ό μ μ©νλ€.
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);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e; // μμΈ λμ§
} finally { // μμΈκ° λ°μνλ, νμ§ μλ νμ μνλμ΄μΌ νλ λΆλΆ(close)μ΄λ―λ‘ finally μ μμ±.
// 컀λ₯μ
(μΈλΆ 리μμ€)μ μ€μ TCP,IP 컀λ₯μ
μ κ±Έλ €μ μ°λ κ²μΌλ‘ μ λ«μΌλ©΄ κ³μ μ μ§λ¨. μμμΌλ‘ λ«μμ€μΌ ν¨
// pstmt.close();
// con.close()
// μμμ μμΈκ° ν°μ§λ©΄ λ«νλ κ² νΈμΆ μμ²΄κ° μ λ μλ μμ΄μ, close() λ©μλλ₯Ό λ§λ€μ΄μ try-catch λ‘ λμνκ² ν¨
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(); // select λ Query λ¬ΈμΌλ‘. μ΄κ±΄ κ²°κ³Όλ₯Ό ResultSet(rs)μ λ΄μμ λ°νν΄μ€λ€.
if (rs.next()) { // rs λ λ΄λΆμ 컀μκ°μ κ² μμ΄μ, ν λ² νΈμΆμ ν΄μ€μΌ μ€μ λ°μ΄ν°κ° μλ κ³³λΆν° μ€νμ΄ λ¨. next() λ 첫 λ²μ§Έ λ°μ΄ν°κ° μλμ§λ₯Ό λ¬Όμ΄λ΄μ true λ©΄ μ§ν
Member member = new Member(); // λ©€λ² κ°μ²΄ λ§λ€μ΄μ μ μ₯ν΄μ£ΌκΈ°
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else { // false λμμ~
throw new NoSuchElementException("member not found memberId=" + memberId); // μμΈλ₯Ό λμ§ λ λ©μμ§λ₯Ό μ λ£λ κ² μ’λ€. λ¬Έμ ν°μ‘μ λ ν΄κ²°νκΈ° μ’μ
}
} catch (SQLException e) {
log.info("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.error("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.error("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);
// JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
// μ£Όμ! νΈλμμ
λκΈ°νλ₯Ό μ¬μ©νλ €λ©΄ DatasourceUtils λ₯Ό μ¬μ©ν΄μ νΈλμμ
λκΈ°ν 맀λμ μ 보κ΄λ 컀λ₯μ
μ κΊΌλ΄μ μ¬μ©νλ€.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("getConnection={}, class={}", con, con.getClass());
return con;
}
}
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.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(money, fromId, toId);
// μ±κ³΅ μ 컀λ°
transactionManager.commit(status);
} catch (Exception e) {
// μ€ν¨ μ λ‘€λ°±
transactionManager.rollback(status);
throw new IllegalStateException(e);
} // μ΄μ release ν νμκ° μμ. νΈλμμ
μ΄ μ»€λ°λκ±°λ λ‘€λ°± λλ©΄ λ€ λλ κ²μ΄κΈ° λλ¬Έμ νΈλμμ
μ΄ μ’
λ£λ¨.(λ μ΄μ 컀λ₯μ
μ μΈ μΌμ΄ μμ
}
private void bizLogic(int money, String fromId, String toId) 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);
}
// μμΈ μν© ν
μ€νΈ νκΈ° μν΄ id ex μΈ κ²½μ° κ²μ¦
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("μ΄μ²΄μ€ μμΈ λ°μ");
}
}
}
private final PlatformTransactionManager transactionManager
DataSourceTransactionManager
ꡬν체λ₯Ό μ£Όμ
λ°μμΌ νλ€.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.MemberRepositoryV2;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
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;
/**
* νΈλμμ
- νΈλμμ
맀λμ
*/
@Slf4j
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(dataSource); // 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("μ΄μ²΄μ€ μμΈ λ°μ")
public void accountTransferEx() throws Exception {
// 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); // μμΈ λ°μνκΈ° λλ¬Έμ rollback λ¨
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
@Test
@DisplayName("μ μ μ΄μ²΄")
public void accountTransfer() throws Exception {
// given
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
// when
log.info("START TX");
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
log.info("END TX");
// then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberB.getMemberId());
Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
Assertions.assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
}
new DataSourceTransactionManager(dataSource)
DataSourceTransactionManager
)λ₯Ό μ νν΄μ μλΉμ€μ μ£Όμ
νλ€.DataSource
κ° νμνλ€.
ν΄λΌμ΄μΈνΈμ μμ²μΌλ‘ μλΉμ€ λ‘μ§μ μ€ννλ€.
1. μλΉμ€ κ³μΈ΅μμ transactionManager.getTransaction()
μ νΈμΆν΄μ νΈλμμ
μ μμνλ€.
2. νΈλμμ
μ μμνλ €λ©΄ λ¨Όμ λ°μ΄ν°λ² μ΄μ€ 컀λ₯μ
μ΄ νμνλ€. νΈλμμ
맀λμ λ λ΄λΆμμ λ°μ΄ν°μμ€λ₯Ό μ¬μ©ν΄μ 컀λ₯μ
μ μμ±νλ€.
3. 컀λ₯μ
μ μλ μ»€λ° λͺ¨λλ‘ λ³κ²½ν΄μ μ€μ λ°μ΄ν°λ² μ΄μ€ νΈλμμ
μ μμνλ€.
4. 컀λ₯μ
μ νΈλμμ
λκΈ°ν 맀λμ μ 보κ΄νλ€.
5. νΈλμμ
λκΈ°ν 맀λμ λ μ°λ λ λ‘컬μ 컀λ₯μ
μ 보κ΄νλ€. λ°λΌμ λ©ν° μ°λ λ νκ²½μ μμ νκ² μ»€λ₯μ
μ 보κ΄ν μ μλ€.
6. μλΉμ€λ λΉμ¦λμ€ λ‘μ§μ μ€ννλ©΄μ 리ν¬μ§ν 리μ λ©μλλ€μ νΈμΆνλ€. μ΄λ 컀λ₯μ
μ νλΌλ―Έν°λ‘ μ λ¬νμ§ μλλ€.
7. 리ν¬μ§ν 리 λ©μλλ€μ νΈλμμ
μ΄ μμλ 컀λ₯μ
μ΄ νμνλ€. 리ν¬μ§ν 리λ DataSourceUtils.getConnection()
μ μ¬μ©ν΄μ νΈλμμ
λκΈ°ν 맀λμ μ 보κ΄λ 컀λ₯μ
μ κΊΌλ΄μ μ¬μ©νλ€. μ΄ κ³Όμ μ ν΅ν΄μ μμ°μ€λ½κ² κ°μ 컀λ₯μ
μ μ¬μ©νκ³ , νΈλμμ
λ μ μ§λλ€.
8. νλν 컀λ₯μ
μ μ¬μ©ν΄μ SQLμ λ°μ΄ν°λ² μ΄μ€μ μ λ¬ν΄μ μ€ννλ€.
9. λΉμ¦λμ€ λ‘μ§μ΄ λλκ³ νΈλμμ
μ μ’
λ£νλ€. νΈλμμ
μ 컀λ°νκ±°λ λ‘€λ°±νλ©΄ μ’
λ£λλ€.
10. νΈλμμ
μ μ’
λ£νλ €λ©΄ λκΈ°νλ 컀λ₯μ
μ΄ νμνλ€. νΈλμμ
λκΈ°ν 맀λμ λ₯Ό ν΅ν΄ λκΈ°νλ 컀λ₯μ
μ νλνλ€.
11. νλν 컀λ₯μ
μ ν΅ν΄ λ°μ΄ν°λ² μ΄μ€μ νΈλμμ
μ 컀λ°νκ±°λ λ‘€λ°±νλ€.
12. μ 체 리μμ€λ₯Ό μ 리νλ€.
con.setAutoCommit(true)
λ‘ λλλ¦°λ€. 컀λ₯μ
νμ κ³ λ €ν΄μΌ νλ€.con.close()
λ₯Ό νΈμΆν΄μ
컀λ₯μ
μ μ’
λ£νλ€. 컀λ₯μ
νμ μ¬μ©νλ κ²½μ° con.close()
λ₯Ό νΈμΆνλ©΄ 컀λ₯μ
νμ λ°νλλ€.νΈλμμ μ¬μ© λ‘μ§ μ€ λ€μ λΆλΆμ΄ λ°λ³΅λλ€.
//νΈλμμ
μμ
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//λΉμ¦λμ€ λ‘μ§
bizLogic(fromId, toId, money);
transactionManager.commit(status); //μ±κ³΅μ 컀λ°
} catch (Exception e) {
transactionManager.rollback(status); //μ€ν¨μ λ‘€λ°±
throw new IllegalStateException(e);
}
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.TransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import java.sql.SQLException;
/**
* νΈλμμ
- νΈλμμ
ν
νλ¦Ώ : λΉμ¦λμ€ μ μΈν λ°λ³΅λλ μ½λ μ κ±°
*/
@Slf4j
public class MemberServiceV3_2 {
// private final PlatformTransactionManager transactionManager;
TransactionTemplate txTemplate;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
private final MemberRepositoryV3 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult((status) -> {
try {
bizLogic(money, fromId, toId);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
private void bizLogic(int money, String fromId, String toId) 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);
}
// μμΈ μν© ν
μ€νΈ νκΈ° μν΄ id ex μΈ κ²½μ° κ²μ¦
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("μ΄μ²΄μ€ μμΈ λ°μ");
}
}
}
MemberServiceV3_2Test
λ κΈ°μ‘΄κ³Ό κ°λ€. ν
μ€νΈλ₯Ό μ€νν΄λ³΄λ©΄ μ μ λμνκ³ , μ€ν¨μ λ‘€λ°±λ μ μνλλ κ²μ νμΈν μ μλ€.μλΉμ€ κ³μΈ΅μ μμν λΉμ¦λμ€ λ‘μ§λ§ λ¨κΈ°λλ‘, μ€νλ§ AOPλ₯Ό ν΅ν΄ νλ‘μλ₯Ό λμ νμ.
@Transactional
μ λ
Έν
μ΄μ
λ§ λΆμ¬μ£Όλ©΄ λλ€. μ€νλ§μ νΈλμμ
AOPλ μ΄ μ λ
Έν
μ΄μ
μ μΈμν΄μ νΈλμμ
νλ‘μλ₯Ό μ μ©ν΄μ€λ€.μ΄λ₯Ό μ μ©ν μλ‘μ΄ μλΉμ€ ν΄λμ€λ₯Ό νμΈν΄λ³΄μ.
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import java.sql.SQLException;
/**
* νΈλμμ
- @Transactional AOP
*/
@Slf4j
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional // ν΄λΉ λ©μλκ° νΈμΆλ λ νΈλμμ
μ κ±Έκ³ μμνλ€. μ±κ³΅νλ©΄ commit, λ°νμ μμΈκ° ν°μ§λ©΄ rollback. μ΄κ² μ΄λ
Έν
μ΄μ
νλλ‘ λλ¨!
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(money, fromId, toId);
}
private void bizLogic(int money, String fromId, String toId) 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);
}
// μμΈ μν© ν
μ€νΈ νκΈ° μν΄ id ex μΈ κ²½μ° κ²μ¦
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("μ΄μ²΄μ€ μμΈ λ°μ");
}
}
}
@Transactional
μ λ
Έν
μ΄μ
μ μΆκ°νλ€.@Transactional
μ λ
Έν
μ΄μ
μ λ©μλμ λΆμ¬λ λκ³ , ν΄λμ€μ λΆμ¬λ λλ€. ν΄λμ€μ λΆμ΄λ©΄ μΈλΆμμ νΈμΆ κ°λ₯ν public
λ©μλκ° AOP μ μ© λμμ΄ λλ€.package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
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.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
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;
/**
* νΈλμμ
- @Transactional AOP
*/
@Slf4j
@SpringBootTest
class MemberServiceV3_3Test {
/**
* κΈ°μ‘΄ before() λ°©μμ μ€νλ§ μ»¨ν
μ΄λμμ μ£Όμ
λ°λ κ² μλλΌ, λ°λ‘λ°λ‘ λ°μ΄ν°μμ€μμ κΊΌλ΄μ°λ λ°©μ.
* μ€νλ§ AOP λ₯Ό μ΄μ©νκΈ° μν΄μλ, (@SpringBootTest λ‘)μ€νλ§ μ»¨ν
μ΄λλ₯Ό μμ±νκ³ (@Autowired λ‘)λΉ λ±λ‘μ νκ³ μ£Όμ
λ°μ μ¨μΌ ν¨.
*/
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
@Autowired
private MemberRepositoryV3 memberRepository;
@Autowired
private MemberServiceV3_3 memberService;
@TestConfiguration
static class TestConfig {
@Bean
DataSource dataSource() {
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
MemberRepositoryV3 memberRepositoryV3() {
return new MemberRepositoryV3(dataSource());
}
@Bean
MemberServiceV3_3 memberServiceV3_3() {
return new MemberServiceV3_3(memberRepositoryV3());
}
}
@Test
void AOPCheck() {
log.info("memberService class={}", memberService.getClass()); // $$SpringCGLIB$$ -> μλΉμ€μ κ²½μ° μ€νλ§μ΄ @Transactional 보μλ§μ 'λλ AOP μ μ© λμμ΄κ΅¬λ!'νλ©΄μ νΈλμμ
νλ‘μλ₯Ό λ§λ€μ΄μ λμ μ μ©λκΈ° λλ¬Έμ ν΄λμ€ μ λ³΄κ° μ΄λ κ² λμ΄? λ€μ νμΈ
log.info("memberRepository class={}", memberRepository.getClass()); // κΈ°λ³Έ ν΄λμ€ μ 보 λμ΄
assertThat(AopUtils.isAopProxy(memberService)).isTrue(); // μλΉμ€λ @Transactional κ±Έλ € μμ΄μ AOP λ§μΌλκΉ true
assertThat(AopUtils.isAopProxy(memberRepository)).isFalse(); // λ ν¬λ AOP μλλκΉ false
// TODO νλ‘μ μ‘°μ¬?
}
@AfterEach
void after() throws SQLException {
memberRepository.delete(MEMBER_A);
memberRepository.delete(MEMBER_B);
memberRepository.delete(MEMBER_EX);
}
@Test
@DisplayName("μ΄μ²΄μ€ μμΈ λ°μ")
public void accountTransferEx() throws Exception {
// 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); // μμΈ λ°μνκΈ° λλ¬Έμ rollback λ¨
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
@Test
@DisplayName("μ μ μ΄μ²΄")
public void accountTransfer() throws Exception {
// given
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
// when
log.info("START TX");
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
log.info("END TX");
// then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberB.getMemberId());
Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
Assertions.assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
}
@SpringBootTest
: μ€νλ§ AOPλ₯Ό μ μ©νλ €λ©΄ μ€νλ§ μ»¨ν
μ΄λκ° νμνλ€. μ΄ μ΄λ
Έν
μ΄μ
μ΄ μμΌλ©΄ ν
μ€νΈ μ μ€νλ§ λΆνΈλ₯Ό ν΅ν΄ μ€νλ§ μ»¨ν
μ΄λλ₯Ό μμ±νλ€. κ·Έλ¦¬κ³ ν
μ€νΈμμ @Autowired
λ±μ ν΅ν΄ μ€νλ§ μ»¨ν
μ΄λκ° κ΄λ¦¬νλ λΉλ€μ μ¬μ©ν μ μλ€.@TestConfiguration
: ν
μ€νΈ μμμ λ΄λΆ μ€μ ν΄λμ€λ₯Ό λ§λ€μ΄μ μ¬μ©νλ©΄μ μ΄ μλ
Έν
μ΄μ
μ λΆμ΄λ©΄, μ€νλ§ λΆνΈκ° μλμΌλ‘ λ§λ€μ΄μ£Όλ λΉλ€μ μΆκ°λ‘ νμν μ€νλ§ λΉλ€μ λ±λ‘νκ³ ν
μ€νΈλ₯Ό μνν μ μλ€.TestConfig
DataSource
μ€νλ§μμ κΈ°λ³ΈμΌλ‘ μ¬μ©ν λ°μ΄ν°μμ€λ₯Ό μ€νλ§ λΉμΌλ‘ λ±λ‘νλ€. μΆκ°λ‘ νΈλμμ
맀λμ μμλ μ¬μ©νλ€.DataSourceTransactionManager
νΈλμμ
맀λμ λ₯Ό μ€νλ§ λΉμΌλ‘ λ±λ‘νλ€. μ€νλ§μ΄ μ 곡νλ νΈλμμ
AOPλ μ€νλ§ λΉμ λ±λ‘λ νΈλμμ
맀λμ λ₯Ό μ°Ύμμ μ¬μ©νκΈ° λλ¬Έμ νΈλμμ
맀λμ λ₯Ό μ€νλ§ λΉμΌλ‘ λ±λ‘ν΄λμ΄μΌ νλ€.AopCheck()
μ μ€ν κ²°κ³Όλ₯Ό 보면 memberService
μ EnhancerBySpringCGLIB..
λΌλ λΆλΆμ ν΅ν΄ νλ‘μ(CGLIB)κ° μ μ©λ κ²μ νμΈν μ μλ€. memberRepository
μλ AOPλ₯Ό μ μ©νμ§ μμκΈ° λλ¬Έμ νλ‘μκ° μ μ©λμ§ μλλ€.@Transactional
μ λ
Έν
μ΄μ
νλλ§ μ μΈν΄μ λ§€μ° νΈλ¦¬νκ² νΈλμμ
μ μ μ©νλ κ²μ μ μΈμ νΈλμ μ
κ΄λ¦¬λΌ νλ€.μ€λ¬΄μμλ λλΆλΆ μ μΈμ νΈλμμ κ΄λ¦¬λ₯Ό μ¬μ©νλ€!
DataSource
)λ₯Ό μ€νλ§ λΉμ μλμΌλ‘ λ±λ‘νλ€.dataSource
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
HikariDataSource
μ΄λ€. 컀λ₯μ
νκ³Ό κ΄λ ¨λ μ€μ λ application.properties
λ₯Ό ν΅ν΄μ μ§μ ν μ μλ€.spring.datasource.url
μμ±μ΄ μμΌλ©΄ λ΄μ₯ λ°μ΄ν°λ² μ΄μ€(λ©λͺ¨λ¦¬ DB)λ₯Ό μμ±νλ €κ³ μλνλ€.PlatformTransactionManager
)λ₯Ό μλμΌλ‘ μ€νλ§ λΉμ λ±λ‘νλ€.transactionManager
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
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;
/**
* νΈλμμ
- @Transactional AOP
* Datasource, Transaction Manager μλ λ±λ‘
*/
@Slf4j
@SpringBootTest
class MemberServiceV3_4Test {
/**
* κΈ°μ‘΄ before() λ°©μμ μ€νλ§ μ»¨ν
μ΄λμμ μ£Όμ
λ°λ κ² μλλΌ, λ°λ‘λ°λ‘ λ°μ΄ν°μμ€μμ κΊΌλ΄μ°λ λ°©μ.
* μ€νλ§ AOP λ₯Ό μ΄μ©νκΈ° μν΄μλ, (@SpringBootTest λ‘)μ€νλ§ μ»¨ν
μ΄λλ₯Ό μμ±νκ³ (@Autowired λ‘)λΉ λ±λ‘μ νκ³ μ£Όμ
λ°μ μ¨μΌ ν¨.
*/
public static final String MEMBER_A = "memberA";
public static final String MEMBER_B = "memberB";
public static final String MEMBER_EX = "ex";
@Autowired
private MemberRepositoryV3 memberRepository;
@Autowired
private MemberServiceV3_3 memberService;
@TestConfiguration
static class TestConfig {
// μ€νλ§μ΄ μλμΌλ‘ μ€νλ§ μ»¨ν
μ΄λμ λ±λ‘ν΄μ€ dataSorce λ₯Ό λ£κ³ , μλ repo- μμ κ°μ Έλ€ μΈ μ μμ.
private final DataSource dataSource;
public TestConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
MemberRepositoryV3 memberRepositoryV3() {
return new MemberRepositoryV3(dataSource);
}
@Bean
MemberServiceV3_3 memberServiceV3_3() {
return new MemberServiceV3_3(memberRepositoryV3());
}
}
@Test
void AOPCheck() {
log.info("memberService class={}", memberService.getClass()); // $$SpringCGLIB$$ -> μλΉμ€μ κ²½μ° μ€νλ§μ΄ @Transactional 보μλ§μ 'λλ AOP μ μ© λμμ΄κ΅¬λ!'νλ©΄μ νΈλμμ
νλ‘μλ₯Ό λ§λ€μ΄μ λμ μ μ©λκΈ° λλ¬Έμ ν΄λμ€ μ λ³΄κ° μ΄λ κ² λμ΄? λ€μ νμΈ
log.info("memberRepository class={}", memberRepository.getClass()); // κΈ°λ³Έ ν΄λμ€ μ 보 λμ΄
assertThat(AopUtils.isAopProxy(memberService)).isTrue(); // μλΉμ€λ @Transactional κ±Έλ € μμ΄μ AOP λ§μΌλκΉ true
assertThat(AopUtils.isAopProxy(memberRepository)).isFalse(); // λ ν¬λ AOP μλλκΉ false
// TODO νλ‘μ μ‘°μ¬?
}
@AfterEach
void after() throws SQLException {
memberRepository.delete(MEMBER_A);
memberRepository.delete(MEMBER_B);
memberRepository.delete(MEMBER_EX);
}
@Test
@DisplayName("μ΄μ²΄μ€ μμΈ λ°μ")
public void accountTransferEx() throws Exception {
// 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); // μμΈ λ°μνκΈ° λλ¬Έμ rollback λ¨
assertThat(findMemberB.getMoney()).isEqualTo(10000);
}
@Test
@DisplayName("μ μ μ΄μ²΄")
public void accountTransfer() throws Exception {
// given
Member memberA = new Member(MEMBER_A, 10000);
Member memberB = new Member(MEMBER_B, 10000);
memberRepository.save(memberA);
memberRepository.save(memberB);
// when
log.info("START TX");
memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
log.info("END TX");
// then
Member findMemberA = memberRepository.findById(memberA.getMemberId());
Member findMemberB = memberRepository.findById(memberB.getMemberId());
Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
Assertions.assertThat(findMemberB.getMoney()).isEqualTo(12000);
}
}
MemberServiceV3_3Test
)κ³Ό κ°μ μ½λμ΄κ³ TestConfig
λΆλΆλ§ λ€λ₯΄λ€.application.properties
μ μ§μ λ μμ±μ μ°Έκ³ ν΄μ λ°μ΄ν°μμ€μ νΈλμμ
맀λμ λ₯Ό μλμΌλ‘ μμ±ν΄μ€λ€.