backend
  • README
  • DOCS
    • Java Docs
    • Servlet Docs
    • JSP Docs
    • DB & SQL Docs
    • Spring Boot Docs
    • Spring Security Docs
    • AWS Docs
  • 설치하기
    • Intellij 설정
  • 자바
    • 01 Java란?
    • 02 자바 시작하기
    • 03 자료형과 연산자
    • 04 제어문
    • 05 메소드
    • 06 클래스 기초
      • Static 보충자료
      • 패키지 보충자료
    • 07 객체지향 프로그래밍
    • 08 클래스 더 알아보기
      • 열거형 ENUM 보충자료
    • 09 클래스와 자료형
      • 다형성 보충자료
      • 제네릭 보충자료
    • 10 컬렉션 프레임워크
      • 컬렉션 프레임워크 보충자료
    • 11 람다식과 함수형 프로그래밍
      • 람다식 보충자료
    • 12 오류 대비하기
      • 오류 보충자료
    • 13 멀티태스킹
      • 멀티태스킹 보충자료
    • 교재보충
      • java.lang
  • 스프링
    • 서블릿, JSP
      • 05 Servlet(서블릿)
        • 서블릿 보충자료
        • 서블릿 추가코드
        • XML, YAML, JSON
      • 06 JSP(자바 서버 페이지)
        • JSP 보충자료
      • 07 JSTL(JSP 스탠다드 태그 라이브러리)
        • JSTL 보충자료
      • 08 Cookie(쿠키), Session(세션)
      • 09 서블릿,필터,리스너
        • 서블릿,필터,리스너 보충자료
      • 11 도서관리 프로젝트 실습
    • Spring Boot
      • 01 스프링 등장 배경, 객체지향
        • 스프링 등장 배경, 객체지향 보충자료
      • 02 IOC(제어의 역전), DI(의존성 주입)
        • IOC 보충자료
        • DI 보충자료
      • 03 스프링 구조
        • 스프링 구조 보충설명
      • 04 테스트코드 실습
      • 05 스프링 빈 설정
        • 스프링 빈 설정 보충자료
      • 06 싱글톤
        • 싱글톤 보충 자료
      • 07 스프링 빈 자동설정
        • 스프링 빈 자동설정 보충자료
      • 08 빈 생명주기
        • 빈 생명주기 보충자료
      • 09 빈 스코프
        • 빈 스코프 보충자료
      • 10 스프링 MVC
        • 스프링 MVC 보충자료
        • 데이터베이스 연동에 필요한 부분
      • 11 Validation(검증)
        • Validation(검증) 보충자료
      • 12 Bean Validation(빈검증)
        • Bean Validation(빈검증) 보충자료
      • 13 예외처리
        • 예외처리 보충자료
      • 14 타입변환
      • 15 JDBC(Java Database Connectivity)
      • 16 커넥션풀
      • 17 트랜잭션
        • 트랜잭션 보충자료
      • 18 JDBC 템플릿 활용
      • 19 MyBatis
      • 20 JPA(Java Persistence API)
      • 22 게시판 프로젝트 실습
    • Spring Security
      • 보안(Security)
      • Spring Security
      • 2. Spring Security 알아보기
        • 보안 위협 실제 사례와 방어 전략
      • 3. Spring Security 기본 동작 흐름
      • 4. Spring Security로 인증 권한 추가하기
        • Spring Security의 인증 및 인가
      • 5. Spring Security에서 세션 관리하기
        • 세션(Session)과 쿠키(Cookie) 비교, 토큰(Token)과의 관계
        • 해싱 및 해싱알고리즘
        • base64
      • 6. Spring Security 악용 보호
        • SameSite
      • 7. Spring Security로 인가 권한 추가하기
      • 8. Bcrypt(비크립트) 암호화
      • OAuth2 적용하기
  • 네트워크
    • HTTP
    • OSI 7계층
  • DB&SQL
    • 01 Database(데이터베이스)와 SQL 개요
    • 02 관계형 모델
    • 03 집합
    • 04 JOIN 연산
    • 05 MySQL
      • 세이브포인트
      • DBeaver, Mysql 오토커밋 설정 관련
    • 06 SQL 기초
      • 예시데이터 쿼리문
    • 07 SQL 실습
      • 실습 스키마
    • 08 Join 활용
      • 실습스키마
    • 09 SQL 활용
      • 실습스키마
    • 10 정규화
      • 실습 스키마
    • 데이터타입
    • 예시 프로젝트 스키마 구성
  • AWS
    • SSL 연결하기
    • 보충설명
Powered by GitBook
On this page
  1. 스프링
  2. Spring Boot

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로 설정된 내부 트랜잭션 (외부와 함께 커밋) 실행 중.");
        }
    }
}
Previous16 커넥션풀Next트랜잭션 보충자료

Last updated 6 months ago