17 트랜잭션
p296 - src/main/java/spring/domain/Account
@Data
public class Account {
private String name;
private int amount;
}
p297, 298 - src/main/java/spring/jdbc/repository/AccountRepository
public class AccountRepository {
// 잔액 조회 메서드
public Account findByName(Connection connection, String name) throws SQLException {
String sql = "SELECT name, amount FROM Account WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, name);
ResultSet rs = null;
try {
rs = pstmt.executeQuery();
if (rs.next()) {
Account account = new Account();
account.setName(rs.getString("name"));
account.setAmount(rs.getInt("amount"));
return account;
} else {
throw new RuntimeException("Account not found: " + name);
}
} finally {
if (rs != null) {
rs.close();
}
pstmt.close();
}
}
// 출금 메서드
public void withdraw(Connection connection, String name, int amount) throws SQLException {
Account account = findByName(connection, name);
if (account.getAmount() < amount) {
throw new RuntimeException("Insufficient balance");
}
String sql = "UPDATE Account SET amount = amount - ? WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setInt(1, amount);
pstmt.setString(2, name);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
// 입금 메서드
public void deposit(Connection connection, String name, int amount) throws SQLException {
String sql = "UPDATE Account SET amount = amount + ? WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setInt(1, amount);
pstmt.setString(2, name);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
// 계좌 생성 메서드
public void createAccount(Connection connection, String name, int amount) throws SQLException {
String sql = "INSERT INTO Account (name, amount) VALUES (?, ?)";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setString(1, name);
pstmt.setInt(2, amount);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
// 계좌 삭제 메서드
public void deleteAccount(Connection connection, String name) throws SQLException {
String sql = "DELETE FROM Account WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setString(1, name);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
}
p299, 300 - src/main/java/spring/jdbc/service/AccountService
@Slf4j
public class AccountService {
private final DataSource dataSource;
private final AccountRepository accountRepository;
public AccountService(DataSource dataSource, AccountRepository accountRepository) {
this.dataSource = dataSource;
this.accountRepository = accountRepository;
}
public void transferMoney(String fromName, String toName, int amount) throws SQLException {
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false); // 트랜잭션 시작
// 출금
accountRepository.withdraw(connection, fromName, amount);
log.info("Trasnsaction started for: {}", fromName);
// 인위적인 지연을 추가하여 트랜잭션 경합 발생 가능성 증가
// 입금
accountRepository.deposit(connection, toName, amount);
// 트랜잭션 커밋
connection.commit();
} catch (SQLTimeoutException e) {
// SQL 타임아웃이 발생하면 락이 걸린 상태로 대기 중일 수 있음
log.info("Lock timeout occurred: " + e.getMessage());
} catch (SQLException e) {
log.info("SQLException occurred: " + e.getMessage());
if (connection != null) {
connection.rollback(); // 오류 발생 시 트랜잭션 롤백
}
} finally {
if (connection != null) {
connection.setAutoCommit(true); // 원래 상태로 복구
connection.close(); // 커넥션 닫기
}
}
}
public void transferMoneyWithDelay(String fromName, String toName, int amount) throws SQLException {
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false); // 트랜잭션 시작
// 출금
accountRepository.withdraw(connection, fromName, amount);
log.info("Trasnsaction started for: {}", fromName);
// 인위적으로 지연을 추가하여 락을 유지한 상태로 둠
Thread.sleep(5000); // 5초 대기
// 입금
accountRepository.deposit(connection, toName, amount);
// 트랜잭션 커밋
connection.commit();
} catch (SQLTimeoutException e) {
// SQL 타임아웃이 발생하면 락이 걸린 상태로 대기 중일 수 있음
log.info("Lock timeout occurred: " + e.getMessage());
} catch (SQLException e) {
log.info("SQLException occurred: " + e.getMessage());
if (connection != null) {
connection.rollback(); // 오류 발생 시 트랜잭션 롤백
}
} catch (InterruptedException e) {
log.info("InterruptedException occured: " + e.getMessage());
} finally {
if (connection != null) {
connection.setAutoCommit(true); // 원래 상태로 복구
connection.close(); // 커넥션 닫기
}
}
}
// 트랜잭션 실패 시 롤백이 이루어지지 않는 상황을 시뮬레이션
public void transferMoneyWithoutRollback(String fromName, String toName, int amount) throws SQLException {
Connection connection = null;
try {
connection = dataSource.getConnection();
// 출금 (성공적으로 처리됨)
accountRepository.withdraw(connection, fromName, amount);
// 인위적인 예외 발생 (입금 중 예외 발생)
if (true) {
throw new RuntimeException("Error during deposit operation");
}
// 입금 (실행되지 않음)
accountRepository.deposit(connection, toName, amount);
} catch (Exception e) {
log.info("Exception occurred: " + e.getMessage());
// 롤백을 수행하지 않음
} finally {
if (connection != null) {
connection.close(); // 커넥션 닫기
}
}
}
// 새로운 메서드 추가: 계좌 조회 서비스 로직
public Account findAccountByName(String name) {
Connection connection = null;
try {
connection = dataSource.getConnection();
return accountRepository.findByName(connection, name);
} catch (SQLException e) {
log.info("SQLException occurred while finding account: " + e.getMessage());
throw new RuntimeException("Error occurred while fetching account: " + name, e);
} finally {
if (connection != null) {
try {
connection.close(); // 커넥션 닫기
} catch (SQLException e) {
log.info("Failed to close connection: " + e.getMessage());
}
}
}
}
}
p301, 302 - src/test/java/spring/jdbc/service/AccountServiceTest
@Slf4j
class AccountServiceTest {
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String USERNAME = "root";
private static final String PASSWORD = "1234";
private DataSource dataSource;
private AccountRepository accountRepository;
private AccountService accountService;
@BeforeEach
public void setUp() throws SQLException {
// HikariCP 설정 및 MySQL 연결
HikariConfig config = new HikariConfig();
config.setJdbcUrl(URL);
config.setUsername(USERNAME);
config.setPassword(PASSWORD);
dataSource = new HikariDataSource(config);
// Repository 및 Service 인스턴스 초기화
accountRepository = new AccountRepository();
accountService = new AccountService(dataSource, accountRepository);
// 계좌 및 유저 생성
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false); // 트랜잭션 시작
// 계좌가 이미 있으면 삭제 후 생성
accountRepository.deleteAccount(connection, "AccountA");
accountRepository.deleteAccount(connection, "AccountB");
accountRepository.createAccount(connection, "AccountA", 1000);
accountRepository.createAccount(connection, "AccountB", 1000);
connection.commit();
}
}
@AfterEach
public void tearDown() throws SQLException {
// 테스트 후 계좌 초기화
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false); // 트랜잭션 시작
// 계좌 삭제
accountRepository.deleteAccount(connection, "AccountA");
accountRepository.deleteAccount(connection, "AccountB");
connection.commit();
}
}
@Test
public void testConcurrentTransactionIssue() throws InterruptedException {
// 스레드 1: 계좌 A에서 B로 송금
Thread thread1 = new Thread(() -> {
try {
accountService.transferMoney("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 1 Error: " + e.getMessage());
}
});
// 스레드 2: 계좌 A에서 B로 동시에 송금 (동일한 자원 접근)
Thread thread2 = new Thread(() -> {
try {
accountService.transferMoney("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 2 Error: " + e.getMessage());
}
});
// 두 스레드를 거의 동시에 시작
thread1.start();
thread2.start();
// 두 스레드가 끝날 때까지 기다림
thread1.join();
thread2.join();
try {
Account accountA = accountService.findAccountByName("AccountA");
Account accountB = accountService.findAccountByName("AccountB");
assertEquals(0, accountA.getAmount()); // AccountA는 0이어야 함
assertEquals(2000, accountB.getAmount()); // AccountB는 2000이어야 함
} catch (Exception e) {
fail("Database error: " + e.getMessage());
}
}
@Test
public void testTransactionLockIssue() throws InterruptedException {
// 스레드 1: 계좌 A에서 B로 송금 (트랜잭션 지연)
Thread thread1 = new Thread(() -> {
try {
accountService.transferMoneyWithDelay("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 1 Error: " + e.getMessage());
}
});
// 스레드 2: 계좌 A에서 B로 동시에 송금 (락이 걸린 상태로 접근 시도)
Thread thread2 = new Thread(() -> {
try {
accountService.transferMoneyWithDelay("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 2 Error: " + e.getMessage());
}
});
// 두 스레드를 거의 동시에 시작
thread1.start();
thread2.start();
// 두 스레드가 끝날 때까지 기다림
thread1.join();
thread2.join();
// 결과 확인: AccountA 잔액 확인 (트랜잭션이 실패하면 그대로 남아있어야 함)
try {
Account accountA = accountService.findAccountByName("AccountA");
Account accountB = accountService.findAccountByName("AccountB");
log.info("accountA getAmout : {}", accountA.getAmount());
log.info("accountB getAmount : {}", accountB.getAmount());
// 두 트랜잭션이 충돌했기 때문에 AccountA의 잔액이 0이 아닐 수도 있음
assertTrue(accountA.getAmount() >= 0 && accountA.getAmount() <= 1000);
assertTrue(accountB.getAmount() >= 1000 && accountB.getAmount() <= 2000);
} catch (Exception e) {
fail("Database error: " + e.getMessage());
}
}
@Test
public void testPartialCommitWithoutRollback() throws InterruptedException, SQLException {
// 트랜잭션이 정상적으로 롤백되지 않는 상황을 테스트
// 출금 후 예외 발생으로 입금은 되지 않지만, 롤백이 발생하지 않음
try {
accountService.transferMoneyWithoutRollback("AccountA", "AccountB", 500);
} catch (Exception e) {
System.out.println("Exception caught in test: " + e.getMessage());
}
// 결과 확인: AccountA에서 500이 인출되고, AccountB는 변동이 없어야 함
try {
Account accountA = accountService.findAccountByName("AccountA");
Account accountB = accountService.findAccountByName("AccountB");
assertEquals(500, accountA.getAmount()); // AccountA에서는 500이 빠져 있어야 함
assertEquals(1000, accountB.getAmount()); // AccountB는 변화가 없어야 함 (입금 실패)
} catch (Exception e) {
fail("Database error: " + e.getMessage());
}
}
}
p303, 304, - src/main/java/spring/jdbc/repository/AccountRepositoryTransactionManager
@Slf4j
public class AccountServiceTransactionManager {
private final PlatformTransactionManager transactionManager;
private final AccountRepositoryTransactionManager accountRepositoryTransactionManager;
public AccountServiceTransactionManager(PlatformTransactionManager transactionManager, AccountRepositoryTransactionManager accountRepositoryTransactionManager) {
this.transactionManager = transactionManager;
this.accountRepositoryTransactionManager = accountRepositoryTransactionManager;
}
public void transferMoney(String fromName, String toName, int amount) throws SQLException {
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
// 입금
accountRepositoryTransactionManager.deposit(toName, amount);
// 트랜잭션 커밋
transactionManager.commit(status);
log.info("Transaction committed for: {} -> {}", fromName, toName);
} catch (Exception e) {
// 트랜잭션 롤백
transactionManager.rollback(status);
log.error("Transaction rolled back due to an error: {}", e.getMessage());
throw e; // 예외 재발생
}
}
@Transactional
public void transferMoneyTransactional(String fromName, String toName, int amount) throws SQLException {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
// 입금
accountRepositoryTransactionManager.deposit(toName, amount);
}
@Transactional
public void transferMoneyTransactionalError(String fromName, String toName, int amount) throws SQLException {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
throw new RuntimeException("exception");
// 입금
// accountRepositoryTransactionManager.deposit(toName, amount);
}
public void transferMoneyWithDelay(String fromName, String toName, int amount) throws SQLException, InterruptedException {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
// 인위적인 지연
Thread.sleep(5000); // 5초 대기
// 입금
accountRepositoryTransactionManager.deposit(toName, amount);
// 트랜잭션 커밋
transactionManager.commit(status);
log.info("Transaction committed for: {} -> {}", fromName, toName);
} catch (Exception e) {
transactionManager.rollback(status);
log.error("Transaction rolled back due to an error: {}", e.getMessage());
throw e;
}
}
public Account findAccountByName(String name) throws SQLException {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 계좌 조회
Account account = accountRepositoryTransactionManager.findByName(name);
transactionManager.commit(status);
return account;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("Transaction rolled back due to an error: {}", e.getMessage());
throw e;
}
}
}
p309 - src/main/java/spring/jdbc/repository/AccountRepositoryTransactionManager
public class AccountRepositoryTransactionManager {
private final DataSource dataSource;
public AccountRepositoryTransactionManager(DataSource dataSource) {
this.dataSource = dataSource;
}
// 공통 PreparedStatement 생성 메서드
private PreparedStatement createPreparedStatement(Connection connection, String sql) throws SQLException {
return connection.prepareStatement(sql);
}
// 자원 해제 공통 메서드
private void closeResources(ResultSet rs, PreparedStatement pstmt, Connection connection) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
DataSourceUtils.releaseConnection(connection, dataSource); // 커넥션 해제
}
}
// 잔액 조회 메서드
public Account findByName(String name) throws SQLException {
String sql = "SELECT name, amount FROM Account WHERE name = ?";
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = createPreparedStatement(connection, sql);
pstmt.setString(1, name); // 파라미터 설정을 여기서 처리
rs = pstmt.executeQuery();
if (rs.next()) {
Account account = new Account();
account.setName(rs.getString("name"));
account.setAmount(rs.getInt("amount"));
return account;
} else {
throw new RuntimeException("Account not found: " + name);
}
} finally {
closeResources(rs, pstmt, connection);
}
}
// 출금 메서드
public void withdraw(String name, int amount) throws SQLException {
Account account = findByName(name);
if (account.getAmount() < amount) {
throw new RuntimeException("Insufficient balance");
}
String sql = "UPDATE Account SET amount = amount - ? WHERE name = ?";
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement pstmt = null;
try {
pstmt = createPreparedStatement(connection, sql);
pstmt.setInt(1, amount); // 파라미터 설정을 여기서 처리
pstmt.setString(2, name);
pstmt.executeUpdate();
} finally {
closeResources(null, pstmt, connection);
}
}
// 입금 메서드
public void deposit(String name, int amount) throws SQLException {
String sql = "UPDATE Account SET amount = amount + ? WHERE name = ?";
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement pstmt = null;
try {
pstmt = createPreparedStatement(connection, sql);
pstmt.setInt(1, amount); // 파라미터 설정을 여기서 처리
pstmt.setString(2, name);
pstmt.executeUpdate();
} finally {
closeResources(null, pstmt, connection);
}
}
// 계좌 생성 메서드
public void createAccount(String name, int amount) throws SQLException {
String sql = "INSERT INTO Account (name, amount) VALUES (?, ?)";
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement pstmt = null;
try {
pstmt = createPreparedStatement(connection, sql);
pstmt.setString(1, name); // 파라미터 설정을 여기서 처리
pstmt.setInt(2, amount);
pstmt.executeUpdate();
} finally {
closeResources(null, pstmt, connection);
}
}
// 계좌 삭제 메서드
public void deleteAccount(String name) throws SQLException {
String sql = "DELETE FROM Account WHERE name = ?";
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement pstmt = null;
try {
pstmt = createPreparedStatement(connection, sql);
pstmt.setString(1, name); // 파라미터 설정을 여기서 처리
pstmt.executeUpdate();
} finally {
closeResources(null, pstmt, connection);
}
}
}
p310 - src/main/java/spring/jdbc/repository/AccountRepository
public class AccountRepository {
// 잔액 조회 메서드
public Account findByName(Connection connection, String name) throws SQLException {
String sql = "SELECT name, amount FROM Account WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, name);
ResultSet rs = null;
try {
rs = pstmt.executeQuery();
if (rs.next()) {
Account account = new Account();
account.setName(rs.getString("name"));
account.setAmount(rs.getInt("amount"));
return account;
} else {
throw new RuntimeException("Account not found: " + name);
}
} finally {
if (rs != null) {
rs.close();
}
pstmt.close();
}
}
// 출금 메서드
public void withdraw(Connection connection, String name, int amount) throws SQLException {
Account account = findByName(connection, name);
if (account.getAmount() < amount) {
throw new RuntimeException("Insufficient balance");
}
String sql = "UPDATE Account SET amount = amount - ? WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setInt(1, amount);
pstmt.setString(2, name);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
// 입금 메서드
public void deposit(Connection connection, String name, int amount) throws SQLException {
String sql = "UPDATE Account SET amount = amount + ? WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setInt(1, amount);
pstmt.setString(2, name);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
// 계좌 생성 메서드
public void createAccount(Connection connection, String name, int amount) throws SQLException {
String sql = "INSERT INTO Account (name, amount) VALUES (?, ?)";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setString(1, name);
pstmt.setInt(2, amount);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
// 계좌 삭제 메서드
public void deleteAccount(Connection connection, String name) throws SQLException {
String sql = "DELETE FROM Account WHERE name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
try {
pstmt.setString(1, name);
pstmt.executeUpdate();
} finally {
pstmt.close();
}
}
}
p311, 312, - src/main/java/spring/jdbc/service/AccountServiceTransactionManager
@Slf4j
public class AccountServiceTransactionManager {
private final PlatformTransactionManager transactionManager;
private final AccountRepositoryTransactionManager accountRepositoryTransactionManager;
public AccountServiceTransactionManager(PlatformTransactionManager transactionManager, AccountRepositoryTransactionManager accountRepositoryTransactionManager) {
this.transactionManager = transactionManager;
this.accountRepositoryTransactionManager = accountRepositoryTransactionManager;
}
public void transferMoney(String fromName, String toName, int amount) throws SQLException {
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
// 입금
accountRepositoryTransactionManager.deposit(toName, amount);
// 트랜잭션 커밋
transactionManager.commit(status);
log.info("Transaction committed for: {} -> {}", fromName, toName);
} catch (Exception e) {
// 트랜잭션 롤백
transactionManager.rollback(status);
log.error("Transaction rolled back due to an error: {}", e.getMessage());
throw e; // 예외 재발생
}
}
@Transactional
public void transferMoneyTransactional(String fromName, String toName, int amount) throws SQLException {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
// 입금
accountRepositoryTransactionManager.deposit(toName, amount);
}
@Transactional
public void transferMoneyTransactionalError(String fromName, String toName, int amount) throws SQLException {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
throw new RuntimeException("exception");
// 입금
// accountRepositoryTransactionManager.deposit(toName, amount);
}
public void transferMoneyWithDelay(String fromName, String toName, int amount) throws SQLException, InterruptedException {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 출금
accountRepositoryTransactionManager.withdraw(fromName, amount);
log.info("Transaction started for: {}", fromName);
// 인위적인 지연
Thread.sleep(5000); // 5초 대기
// 입금
accountRepositoryTransactionManager.deposit(toName, amount);
// 트랜잭션 커밋
transactionManager.commit(status);
log.info("Transaction committed for: {} -> {}", fromName, toName);
} catch (Exception e) {
transactionManager.rollback(status);
log.error("Transaction rolled back due to an error: {}", e.getMessage());
throw e;
}
}
public Account findAccountByName(String name) throws SQLException {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 계좌 조회
Account account = accountRepositoryTransactionManager.findByName(name);
transactionManager.commit(status);
return account;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("Transaction rolled back due to an error: {}", e.getMessage());
throw e;
}
}
}
p313 - src/test/java/spring/jdbc/service/AccountServiceTransactionManagerTest
@Slf4j
class AccountServiceTransactionManagerTest {
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String USERNAME = "root";
private static final String PASSWORD = "1234";
private DataSource dataSource;
private AccountRepositoryTransactionManager accountRepository;
private AccountServiceTransactionManager accountService;
@BeforeEach
public void setUp() throws SQLException {
// HikariCP 설정 및 MySQL 연결
HikariConfig config = new HikariConfig();
config.setJdbcUrl(URL);
config.setUsername(USERNAME);
config.setPassword(PASSWORD);
dataSource = new HikariDataSource(config);
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
// Repository 및 Service 인스턴스 초기화
accountRepository = new AccountRepositoryTransactionManager(dataSource);
accountService = new AccountServiceTransactionManager(transactionManager, accountRepository);
// 계좌 및 유저 생성
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false); // 트랜잭션 시작
// 계좌가 이미 있으면 삭제 후 생성
accountRepository.deleteAccount("AccountA");
accountRepository.deleteAccount("AccountB");
accountRepository.createAccount("AccountA", 1000);
accountRepository.createAccount("AccountB", 1000);
connection.commit();
} catch (Exception e) {
throw e;
}
}
@AfterEach
public void tearDown() throws SQLException {
// 테스트 후 계좌 초기화
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false); // 트랜잭션 시작
// 계좌 삭제
accountRepository.deleteAccount("AccountA");
accountRepository.deleteAccount("AccountB");
connection.commit();
}
}
@Test
public void testConcurrentTransactionIssue() throws InterruptedException {
// 스레드 1: 계좌 A에서 B로 송금
Thread thread1 = new Thread(() -> {
try {
accountService.transferMoney("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 1 Error: " + e.getMessage());
}
});
// 스레드 2: 계좌 A에서 B로 동시에 송금 (동일한 자원 접근)
Thread thread2 = new Thread(() -> {
try {
accountService.transferMoney("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 2 Error: " + e.getMessage());
}
});
// 두 스레드를 거의 동시에 시작
thread1.start();
thread2.start();
// 두 스레드가 끝날 때까지 기다림
thread1.join();
thread2.join();
try {
Account accountA = accountService.findAccountByName("AccountA");
Account accountB = accountService.findAccountByName("AccountB");
assertEquals(0, accountA.getAmount()); // AccountA는 0이어야 함
assertEquals(2000, accountB.getAmount()); // AccountB는 2000이어야 함
} catch (Exception e) {
fail("Database error: " + e.getMessage());
}
}
@Test
public void testTransactionLockIssue() throws InterruptedException {
// 스레드 1: 계좌 A에서 B로 송금 (트랜잭션 지연)
Thread thread1 = new Thread(() -> {
try {
accountService.transferMoneyWithDelay("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 1 Error: " + e.getMessage());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 스레드 2: 계좌 A에서 B로 동시에 송금 (락이 걸린 상태로 접근 시도)
Thread thread2 = new Thread(() -> {
try {
accountService.transferMoneyWithDelay("AccountA", "AccountB", 500);
} catch (SQLException e) {
System.out.println("Thread 2 Error: " + e.getMessage());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 두 스레드를 거의 동시에 시작
thread1.start();
thread2.start();
// 두 스레드가 끝날 때까지 기다림
thread1.join();
thread2.join();
// 결과 확인: AccountA 잔액 확인 (트랜잭션이 실패하면 그대로 남아있어야 함)
try {
Account accountA = accountService.findAccountByName("AccountA");
Account accountB = accountService.findAccountByName("AccountB");
log.info("accountA getAmout : {}", accountA.getAmount());
log.info("accountB getAmount : {}", accountB.getAmount());
// 두 트랜잭션이 충돌했기 때문에 AccountA의 잔액이 0이 아닐 수도 있음
assertTrue(accountA.getAmount() >= 0 && accountA.getAmount() <= 1000);
assertTrue(accountB.getAmount() >= 1000 && accountB.getAmount() <= 2000);
} catch (Exception e) {
fail("Database error: " + e.getMessage());
}
}
}
p318, 320, 321 ~ 324 - src/test/java/spring/jdbc/service/AccountServiceTransactionTest
@Slf4j
@SpringBootTest
public class AccountServiceTransactionTest {
// 나중에 application.properties로 뺄 거임 환경설정
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String USERNAME = "root";
private static final String PASSWORD = "1234";
@Autowired
private DataSource dataSource;
@Autowired
private AccountRepositoryTransactionManager accountRepository;
@Autowired
private AccountServiceTransactionManager accountService;
@TestConfiguration
static class TestConfig {
@Bean
public DataSource dataSource() {
// HikariCP 설정 및 MySQL 연결
HikariConfig config = new HikariConfig();
config.setJdbcUrl(URL);
config.setUsername(USERNAME);
config.setPassword(PASSWORD);
return new HikariDataSource(config);
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public AccountRepositoryTransactionManager accountRepositoryTransactionManager(DataSource dataSource) {
return new AccountRepositoryTransactionManager(dataSource);
}
@Bean
public AccountServiceTransactionManager accountServiceTransactionManager(PlatformTransactionManager transactionManager,
AccountRepositoryTransactionManager accountRepositoryTransactionManager) {
return new AccountServiceTransactionManager(transactionManager, accountRepositoryTransactionManager);
}
}
@BeforeEach
public void setUp() throws SQLException {
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false); // 트랜잭션 시작
// 계좌가 이미 있으면 삭제 후 생성
accountRepository.deleteAccount("AccountA");
accountRepository.deleteAccount("AccountB");
accountRepository.createAccount("AccountA", 1000);
accountRepository.createAccount("AccountB", 1000);
connection.commit();
} catch (Exception e) {
throw e;
}
}
@AfterEach
public void tearDown() throws SQLException {
// 테스트 후 계좌 초기화
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false); // 트랜잭션 시작
// 계좌 삭제
accountRepository.deleteAccount("AccountA");
accountRepository.deleteAccount("AccountB");
connection.commit();
}
}
@Test
public void testTransferMoneyTransactional() throws SQLException {
// 초기 상태 확인
Account accountA = accountRepository.findByName("AccountA");
Account accountB = accountRepository.findByName("AccountB");
assertEquals(1000, accountA.getAmount());
assertEquals(1000, accountB.getAmount());
// 계좌 A에서 B로 500원을 송금
accountService.transferMoneyTransactional("AccountA", "AccountB", 500);
// 송금 후 상태 확인
accountA = accountRepository.findByName("AccountA");
accountB = accountRepository.findByName("AccountB");
assertEquals(500, accountA.getAmount()); // AccountA는 500원 차감되어야 함
assertEquals(1500, accountB.getAmount()); // AccountB는 500원 증가해야 함
}
@Test
public void testTransferMoneyTransactionalErrorRollback() throws SQLException {
// 초기 상태 확인
Account accountA = accountRepository.findByName("AccountA");
Account accountB = accountRepository.findByName("AccountB");
assertEquals(1000, accountA.getAmount());
assertEquals(1000, accountB.getAmount());
// 예외가 발생하여 트랜잭션이 롤백되는지 테스트
Exception exception = assertThrows(RuntimeException.class, () -> {
accountService.transferMoneyTransactionalError("AccountA", "AccountB", 500);
});
// 예외 메시지 확인
assertEquals("exception", exception.getMessage());
// 트랜잭션 롤백 후 상태 확인
accountA = accountRepository.findByName("AccountA");
accountB = accountRepository.findByName("AccountB");
// 롤백으로 인해 잔액이 변경되지 않았음을 확인
assertEquals(1000, accountA.getAmount()); // AccountA는 여전히 1000원이어야 함 (롤백됨)
assertEquals(1000, accountB.getAmount()); // AccountB도 여전히 1000원이어야 함 (롤백됨)
}
}
p325 - src/test/resources/application.properties
# Application Name
spring.application.name=template
# MySQL Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=1234
# HikariCP Configuration
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.pool-name=HikariPool
logging.level.org.springframework.transaction=TRACE
logging.level.org.springframework.transaction.interceptor.TransactionInterceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=TRACE
logging.level.org.springframework.jdbc.core=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE
logging.level.org.mybatis.spring.SqlSessionTemplate=DEBUG
logging.level.jdbc.sqlonly=TRACE
p326, 327 - src/test/java/spring/jdbc/transaction
@Slf4j
@SpringBootTest // 메인 클래스 추가
public class TransactionTest {
@Autowired
private TestService testService;
@Test
void proxyClassTest() {
log.info("service class = {}", testService.getClass());
Assertions.assertThat(AopUtils.isAopProxy(testService)).isTrue();
}
@Test
void execMethod() {
testService.annoTx();
testService.nonTx();
}
@TestConfiguration
static class TestConfig {
@Bean
TestService testService() {
return new TestService();
}
}
@Slf4j
static class TestService {
@Transactional
public void annoTx() {
log.info("annot Tx");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("annot Tx isActive = {}", isActive);
}
public void nonTx() {
log.info("non Tx");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("non Tx isActive = {}", isActive);
}
}
}
p328 - src/test/java/spring/jdbc/transaction/TransactionLevelTest
@Slf4j
@SpringBootTest
public class TransactionLevelTest {
@Autowired
private TransactionalService transactionalService;
@Test
void transactionPriorityTest() {
transactionalService.classTransactionalMethod();
transactionalService.methodTransactionalMethod();
}
@TestConfiguration
static class TestConfig {
@Bean
public TransactionalService transactionalService() {
return new TransactionalService();
}
}
@Slf4j
@Transactional(readOnly = true) // 클래스 레벨 트랜잭션 설정
static class TransactionalService {
public void classTransactionalMethod() {
log.info("Executing classTransactionalMethod");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("classTransactionalMethod - Transaction active: {}, readOnly: {}", isActive, isReadOnly);
}
@Transactional // 메서드 레벨 트랜잭션 설정 (기본 설정, 읽기 전용 아님)
public void methodTransactionalMethod() {
log.info("Executing methodTransactionalMethod");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("methodTransactionalMethod - Transaction active: {}, readOnly: {}", isActive, isReadOnly);
}
}
}
p329 - src/test/java/spring/jdbc/transaction/TransactionActiveTest
@Slf4j
@SpringBootTest
public class TransactionLevelTest {
@Autowired
private TransactionalService transactionalService;
@Test
void transactionPriorityTest() {
transactionalService.classTransactionalMethod();
transactionalService.methodTransactionalMethod();
}
@TestConfiguration
static class TestConfig {
@Bean
public TransactionalService transactionalService() {
return new TransactionalService();
}
}
@Slf4j
@Transactional(readOnly = true) // 클래스 레벨 트랜잭션 설정
static class TransactionalService {
public void classTransactionalMethod() {
log.info("Executing classTransactionalMethod");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("classTransactionalMethod - Transaction active: {}, readOnly: {}", isActive, isReadOnly);
}
@Transactional // 메서드 레벨 트랜잭션 설정 (기본 설정, 읽기 전용 아님)
public void methodTransactionalMethod() {
log.info("Executing methodTransactionalMethod");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
log.info("methodTransactionalMethod - Transaction active: {}, readOnly: {}", isActive, isReadOnly);
}
}
}
p330, 331 - src/test/java/spring/jdbc/transaction/TransactionNotApplied
@Slf4j
@SpringBootTest
public class TransactionNotApplied {
@Autowired
private TestService testService;
@Test
void selfInvocationTest() {
log.info("Self-invocation Test Start");
testService.outerMethod();
}
@Test
void staticMethodTest() {
log.info("Static Method Test Start");
TestService.staticTransactionalMethod();
}
@Test
void directInvocationTest() {
log.info("Direct Invocation Test Start");
testService.nonTransactionalMethod();
}
@TestConfiguration
static class TestConfig {
@Bean
public TestService testService() {
return new TestService();
}
}
@Slf4j
static class TestService {
// Self-invocation example
public void outerMethod() {
log.info("Outer method called");
innerTransactionalMethod();
}
@Transactional
public void innerTransactionalMethod() {
log.info("Inner transactional method called");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Inner method transaction active: {}", isActive);
}
// Static method example
@Transactional
public static void staticTransactionalMethod() {
log.info("Static transactional method called");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Static method transaction active: {}", isActive);
}
// Direct invocation example
public void nonTransactionalMethod() {
log.info("Non-transactional method called");
nonAnnotatedMethod();
}
@Transactional
public void nonAnnotatedMethod() {
log.info("Non-annotated transactional method called");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Non-annotated method transaction active: {}", isActive);
}
}
}
p332 - src/test/java/spring/jdbc/transaction/PostConstructTransactionTest
@Slf4j
@SpringBootTest
public class PostConstructTransactionTest {
@Autowired
private TestClassActive ts;
@Test
void contextLoads() {
// 테스트가 정상적으로 ApplicationReadyEvent까지 실행되는지 확인
ts.manualInvoke(); // 수동으로 메서드 호출하여 트랜잭션 상태 확인
}
@Slf4j
@Service
static class TestClassActive {
@PostConstruct
@Transactional
public void initPostConstruct() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("TestClassActive init @PostConstruct tx active={}", isActive);
}
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initApplicationReadyEvent() {
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("TestClassActive init ApplicationReadyEvent tx active={}", isActive);
}
public void manualInvoke() {
log.info("Manual method invoked to check transaction state post-startup");
boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
log.info("Manual invocation - Transaction active: {}", isActive);
}
}
@TestConfiguration
static class TestConfig {
@Bean
public TestClassActive ts() {
return new TestClassActive();
}
}
}
p336(왼쪽이미지) - src/test/java/spring/jdbc/exception/CheckedExceptionServiceTest
// 커스텀 체크 예외 클래스
class CustomCheckedException extends Exception {
public CustomCheckedException(String message) {
super(message);
}
}
// 체크 예외를 발생시키는 서비스 클래스
class CheckedExceptionService {
// 커스텀 체크 예외를 발생시키는 메서드
public void throwCustomCheckedException() throws CustomCheckedException {
// 일부러 체크 예외를 발생시킴
throw new CustomCheckedException("This is a custom checked exception");
}
}
// 체크 예외를 테스트하는 클래스
public class CheckedExceptionServiceTest {
private final CheckedExceptionService checkedExceptionService = new CheckedExceptionService();
// 커스텀 체크 예외가 발생하는지 테스트하는 메서드
@Test
public void testThrowCustomCheckedException() {
// 커스텀 체크 예외가 발생하는지 확인
assertThrows(CustomCheckedException.class, () -> {
checkedExceptionService.throwCustomCheckedException(); // 반드시 처리하거나 던져야 함
});
}
// 커스텀 체크 예외를 처리하는 테스트
@Test
public void testHandleCustomCheckedException() {
// 커스텀 체크 예외를 처리하는 테스트
try {
checkedExceptionService.throwCustomCheckedException(); // 예외를 던짐
} catch (CustomCheckedException e) {
// 예외를 처리하고, 메시지를 확인
System.out.println("Caught custom checked exception: " + e.getMessage());
assertEquals("This is a custom checked exception", e.getMessage());
}
}
}
p336(오른쪽이미지) - src/test/java/spring/jdbc/exception/UncheckedExceptionServiceTest
// 커스텀 언체크 예외 클래스
class CustomUncheckedException extends RuntimeException {
public CustomUncheckedException(String message) {
super(message);
}
}
// 언체크 예외를 발생시키는 서비스 클래스
// 언체크 예외는 던지는 것 생략해도 됨 자연스럽게 상위로 넘어감
class UncheckedExceptionService {
// 커스텀 언체크 예외를 발생시키는 메서드
public void throwCustomUncheckedException() {
// 일부러 언체크 예외를 발생시킴
throw new CustomUncheckedException("This is a custom unchecked exception");
}
// 정상적인 동작을 하는 메서드 (예외 발생 없음)
public void normalOperation() {
System.out.println("Normal operation executed successfully.");
}
}
// 언체크 예외를 테스트하는 클래스
public class UncheckedExceptionServiceTest {
private final UncheckedExceptionService uncheckedExceptionService = new UncheckedExceptionService();
// 커스텀 언체크 예외가 발생하는지 테스트하는 메서드
@Test
public void testThrowCustomUncheckedException() {
// 커스텀 언체크 예외가 발생하는지 확인
assertThrows(CustomUncheckedException.class, () -> {
uncheckedExceptionService.throwCustomUncheckedException(); // 예외 발생
});
}
// 커스텀 언체크 예외가 처리되었는지 테스트
@Test
public void testHandleCustomUncheckedException() {
// 커스텀 언체크 예외를 처리하는 테스트
try {
uncheckedExceptionService.throwCustomUncheckedException(); // 예외를 던짐
} catch (CustomUncheckedException e) {
// 예외를 처리하고, 메시지를 확인
System.out.println("Caught custom unchecked exception: " + e.getMessage());
assertEquals("This is a custom unchecked exception", e.getMessage());
}
}
// 정상적인 동작을 테스트하는 메서드
@Test
public void testNormalOperation() {
// 예외가 발생하지 않는 정상적인 메서드 호출
uncheckedExceptionService.normalOperation(); // 정상 동작 호출
}
}
p338 - src/test/resources/application.properties
# Application Name
spring.application.name=template
# MySQL Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=1234
# HikariCP Configuration
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.pool-name=HikariPool
logging.level.org.springframework.transaction=TRACE
logging.level.org.springframework.transaction.interceptor.TransactionInterceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=TRACE
logging.level.org.springframework.jdbc.core=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE
logging.level.org.mybatis.spring.SqlSessionTemplate=DEBUG
logging.level.jdbc.sqlonly=TRACE
p339 - src/test/java/spring/jdbc/transaction/TransactionExceptionTest
@Slf4j
@SpringBootTest
public class TransactionExceptionTest {
@Autowired
private TransactionalService transactionalService;
@TestConfiguration
static class TestConfig {
@Bean
TransactionalService transactionalService() {
return new TransactionalService();
}
}
@Test
void uncheckedExceptionTest() {
try {
transactionalService.methodWithUncheckedException();
} catch (RuntimeException e) {
log.info("Caught unchecked exception: {}", e.getMessage());
}
}
@Test
void checkedExceptionTest() {
try {
transactionalService.methodWithCheckedException();
} catch (Exception e) {
log.info("Caught checked exception: {}", e.getMessage());
}
}
@Test
void checkedExceptionWithRollbackTest() {
try {
transactionalService.methodWithCheckedExceptionRollback();
} catch (Exception e) {
log.info("Caught checked exception (with rollback): {}", e.getMessage());
}
}
@Slf4j
static class TransactionalService {
@Transactional // 런타임 예외 발생 시 롤백
public void methodWithUncheckedException() {
log.info("Starting transaction for unchecked exception");
registerTransactionStatusLog();
throw new IllegalArgumentException("Unchecked exception occurred");
}
@Transactional // 체크 예외 발생 시 롤백 없이 커밋됨
public void methodWithCheckedException() throws Exception {
log.info("Starting transaction for checked exception");
registerTransactionStatusLog();
throw new Exception("Checked exception occurred");
}
@Transactional(rollbackFor = Exception.class) // 체크 예외 발생 시 롤백 수행
public void methodWithCheckedExceptionRollback() throws Exception {
log.info("Starting transaction for checked exception with rollback");
registerTransactionStatusLog();
throw new Exception("Checked exception occurred (rollback enabled)");
}
private void registerTransactionStatusLog() {
// 트랜잭션 종료 후 커밋 또는 롤백 여부 확인
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCompletion(int status) {
if (status == STATUS_COMMITTED) {
log.info("Transaction was committed.");
} else if (status == STATUS_ROLLED_BACK) {
log.info("Transaction was rolled back.");
}
}
});
}
}
}
p347 ~ 350 - src/test/java/spring/jdbc/transaction/NestedTransactionTest
@Slf4j
@SpringBootTest
public class NestedTransactionTest {
@Autowired
private OuterService outerService;
@TestConfiguration
static class TestConfig {
@Autowired
private DataSource dataSource; // DataSource를 자동 주입
@Bean
OuterService outerService() {
return new OuterService(innerService());
}
@Bean
InnerService innerService() {
return new InnerService(transactionManager());
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource); // DataSource 주입
}
}
@Test
@DisplayName("REQUIRES_NEW 트랜잭션 롤백, REQUIRED 트랜잭션 커밋, 외부 트랜잭션 커밋")
void testInnerRollbackRequiredCommitOuterCommit() {
outerService.outerMethodWithMixedInnerTransactionsCommit();
}
@Slf4j
static class OuterService {
private final InnerService innerService;
public OuterService(InnerService innerService) {
this.innerService = innerService;
}
@Transactional
public void outerMethodWithMixedInnerTransactionsCommit() {
log.info("외부 트랜잭션 시작: 혼합된 내부 트랜잭션 포함");
// 첫 번째 내부 트랜잭션 - REQUIRES_NEW (롤백 유도)
try {
innerService.innerMethodWithRequiresNewRollback();
} catch (RuntimeException e) {
log.info("내부 REQUIRES_NEW 트랜잭션 롤백 발생으로 인한 예외: {}", e.getMessage());
}
// 두 번째 내부 트랜잭션 - REQUIRED (커밋)
innerService.innerMethodWithRequired();
log.info("외부 트랜잭션 완료 후 상태 확인");
}
}
@Slf4j
static class InnerService {
private final PlatformTransactionManager transactionManager;
public InnerService(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// 독립적인 롤백을 수행하는 REQUIRES_NEW 트랜잭션 메서드
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethodWithRequiresNewRollback() {
log.info("REQUIRES_NEW로 설정된 내부 트랜잭션 (롤백) 실행 중.");
// 트랜잭션 완료 후 상태를 확인하기 위한 Synchronization 등록
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
log.info("REQUIRES_NEW 트랜잭션이 커밋되었습니다.");
} else if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
log.info("REQUIRES_NEW 트랜잭션이 롤백되었습니다.");
}
}
});
// 강제 예외 발생으로 트랜잭션 롤백 유도
throw new RuntimeException("강제 예외 발생으로 REQUIRES_NEW 트랜잭션 롤백 유도");
}
// 외부 트랜잭션에 종속적으로 작동하며 커밋됨
@Transactional(propagation = Propagation.REQUIRED)
public void innerMethodWithRequired() {
log.info("REQUIRED로 설정된 내부 트랜잭션 (외부와 함께 커밋) 실행 중.");
}
}
}
Last updated