[개발 일기] 2025.02.16 - Transaction Synchronization

2025. 2. 16. 15:16·개발 일기

💡 개요

오늘은 JDBC에서 사용되는 트랜잭션 동기화(Transaction Synchronization)에 대해 정리해보자.

 

 

📕 Transaction Synchronization

트랜잭션 동기화를 알기 위해선 일단 데이터베이스에 접근하기 위해 사용되는 Connection 객체와 트랜잭션에 대해 알고 있어야 한다.

 

 

🚀 Connection

 

JDBC 환경에서 커넥션이란 데이터베이스와 통신을 위해 사용되는 객체이다.

 

 

즉, 커넥션을 통해 SQL 실행을 위한 객체를 생성하고 트랜잭션을 관리할 수 있다.

 

 

그리고 커넥션 객체 생성은 Connection Pool에 미리 생성되어있는 객체를 받아 사용한다.

 

 

 

🚀 트랜잭션

 

트랜잭션이란 데이터베이스의 상태를 변경하기 위해 수행되는 작업의 단위를 말한다.

 

 

만약 하나의 작업이라도 정상적으로 종료되지 않는다면 데이터의 정합성・무결성을 위해 트랜잭션 내부의 작업을 취소(Rollback)하고, 모든 작업이 정상적으로 종료되면 그 때 데이터베이스에 적용(Commit)하는 것이다.

 

 

그렇기 때문에 트랜잭션을 적용하려면 트랜잭션 범위 내의 모든 데이터베이스 작업이 동일한 커넥션에서 수행되어야 한다.

 

 

 

📕 TransactionSynchronization 구현

다시 트랜잭션 동기화로 돌아오면 트랜잭션 동기화는 매번 같은 커넥션을 Repository 객체에게 전달하는 불편함을 해소한 기술이다.

 

 

말만 들으면 이해가 어려우므로, 아래의 코드를 보자.

 

 

다음 코드는 트랜잭션 동기화를 사용하지 않은 상태로 게시글을 생성하기 위해 작성자 정보를 조회한 후, 게시글을 INSERT하는 상황이다.

 

public class BoardService {
    private MemberRepository memberRepository;
    private BoardRepository boardRepository;

    public BoardService(MemberRepository memberRepository, BoardRepository boardRepository) {
        this.memberRepository = memberRepository;
        this.boardRepository = boardRepository;
    }

    public void saveBoard(String email, Board board) {
        Connection connection = null;
        try {
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
            connection.setAutoCommit(false);

            Long memberId = memberRepository.findMemberByEmail(email, connection);
            boardRepository.saveBoard(memberId, board, connection);

            connection.commit();
        } catch (Exception e) {
            if (connection != null) {
                try {
                    connection.rollback();
                } catch (SQLException rollbackEx) {
                    rollbackEx.printStackTrace();
                }
            }
            throw new RuntimeException("트랜잭션 에러 발생", e);
        } ...
    }
}

 

public class MemberRepository {
    public Long findMemberByEmail(String email, Connection connection) {
        String sql = "SELECT * FROM members WHERE email = ?";
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setString(1, email);
            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                Long memberId = rs.getLong("id");
                return memberId;
            }
            throw new NoDataException("사용자 정보 없음");
        } catch (SQLException e) {
            throw new RuntimeException("데이터베이스 접근 에러 발생", e);
        }
    }
}

 

public class BoardRepository {
    public void saveBoard(Long memberId, Board board, Connection connection) {
        String sql = "INSERT INTO boards (title, content, member_id) VALUES (?, ?, ?)";
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setString(1, board.getTitle());
            pstmt.setString(2, board.getContent());
            pstmt.setLong(3, board.getMemberId());
            pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("데이터베이스 접근 에러 발생", e);
        }
    }
}

 

 

이 코드에서 주목해야 할 점은 BoardRepository.saveBoard(), MemberRepository.findMemberByEmail() 메서드의 파라미터인 Connection connection 이다.

 

 

BoardService 코드를 보면 알 수 있듯이 위의 작업은 모두 트랜잭션 처리가 되어있는 상황이다.

 

 

이를 위해 Service 계층에서 커넥션을 생성한 후, 해당 커넥션을 각각의 Repository 에 전달하는 것이다.

 

 

만약 트랜잭션 동기화를 사용한다면 코드가 다음과 같이 변한다.

 

public class BoardService {
    private final MemberRepository memberRepository;
    private final BoardRepository boardRepository;
    private final DataSourceTransactionManager transactionManager;
    private final DataSource dataSource;

    ...

    public void saveBoard(String email, Board board) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED));

        try {
            TransactionSynchronizationManager.bindResource(dataSource, DataSourceUtils.getConnection(dataSource));

            Long memberId = memberRepository.findMemberByEmail(email);
            boardRepository.saveBoard(memberId, board);

            transactionManager.commit(status);
        } catch (Exception e) {
            // 트랜잭션 롤백
            transactionManager.rollback(status);
            throw new RuntimeException("트랜잭션 에러 발생", e);
        } finally {
            // 트랜잭션 동기화 해제
            TransactionSynchronizationManager.unbindResource(dataSource);
        }
    }
}

 

public class MemberRepository {

    private final DataSource dataSource;
    
    ...
    
    public Long findMemberByEmail(String email) {
        String sql = "SELECT * FROM members WHERE email = ?";
        
        // 커넥션 획득
        Connection connection = DataSourceUtils.getConnection(dataSource);
        
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setString(1, email);
            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                Long memberId = rs.getLong("id");
                return memberId;
            }
            throw new NoDataException("사용자 정보 없음");
        } catch (SQLException e) {
            throw new RuntimeException("데이터베이스 접근 에러 발생", e);
        }
    }
}

 

public class BoardRepository {

    private final DataSource dataSource;
    
    ...
    
    public void saveBoard(Long memberId, Board board) {
        String sql = "INSERT INTO boards (title, content, member_id) VALUES (?, ?, ?)";
        
        // 커넥션 획득
        Connection connection = DataSourceUtils.getConnection(dataSource);
        
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setString(1, board.getTitle());
            pstmt.setString(2, board.getContent());
            pstmt.setLong(3, board.getMemberId());
            pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("데이터베이스 접근 에러 발생", e);
        }
    }
}

 

BoardService.saveBoard() 에서 TransactionSynchronizationManager 을 통해 try-catch 문 내부에서 수행되는 작업을 모두 동기화 커넥션을 사용해 작업을 처리할 수 있도록 했다.

 

 

그리고 MemberRepository.findMemberByEmail(), BoardRepository.saveBoard() 를 보면 모두 파라미터로 커넥션을 받지않고, DataSource 에게 받아서 사용한다.

 

 

하지만 아직 BoardService 에 커넥션 객체 생성 코드와, 트랜잭션 설정 코드가 남아있다.

 

 

이를 코드 상에서 제거하기 위해 PlatformTransactionManager 인터페이스를 사용한다.

 

 

 

🚀 PlatformTransactionManager

 

PlatformTransactionManager 인터페이스를 사용하면 트랜잭션 동기화 적용 뿐만 아니라, 트랜잭션 범위를 지정할 수 있다.

 

 

다음은 PlatformTransactionManager 가 적용된 게시글 저장 코드이다.

 

public class BoardService {
    private final MemberRepository memberRepository;
    private final BoardRepository boardRepository;
    private final DataSource dataSource;
    
    ...

    public void saveBoard(String email, Board board) {
        // 트랜잭션 정의
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            Long memberId = memberRepository.findMemberByEmail(email);
            boardRepository.saveBoard(memberId, board);

            // 성공 시 커밋
            transactionManager.commit(status);
        } catch (Exception e) {
            // 실패 시 롤백
            transactionManager.rollback(status);
            throw new RuntimeException("트랜잭션 에러 발생", e);
        }
    }
}

 

보다시피 BoardService 코드가 훨씬 간결해져 가독성도 높아졌고, 커넥션 객체 생성이나 트랜잭션 설정과 같은 불필요한 의존 관계도 사라졌다.

 

 

 

🤔 결론

물론 Spring Framework에서 지원하는 @Transactional 어노테이션같은 기술을 사용해 손쉽게 트랜잭션 동기화를 구현할 수 있다.

 

 

그래도 @Transactional 내부에서 어떤 작업이 수행되는 지 알고 있으면 좋지 않을까..?

'개발 일기' 카테고리의 다른 글

[개발 일기] 2025.02.18 - 한글과 VARCHAR  (0) 2025.02.18
[개발 일기] 2025.02.17 - Service 여러 개 Repository (Feat : Facade 패턴)  (0) 2025.02.17
[개발 일기] 2025.02.15 - Security Context  (0) 2025.02.15
[개발 일기] 2025.02.14 - 우선순위 큐  (0) 2025.02.14
[개발 일기] 2025.02.13 - 자바 record  (0) 2025.02.13
'개발 일기' 카테고리의 다른 글
  • [개발 일기] 2025.02.18 - 한글과 VARCHAR
  • [개발 일기] 2025.02.17 - Service 여러 개 Repository (Feat : Facade 패턴)
  • [개발 일기] 2025.02.15 - Security Context
  • [개발 일기] 2025.02.14 - 우선순위 큐
오도형석
오도형석
  • 오도형석
    형석이의 성장일기
    오도형석
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • MSA 모니터링 서비스
        • DB
      • 스파르타 코딩클럽
        • SQL
        • Spring
      • 백엔드
        • Internet
        • Java
        • DB
      • 캡스톤
        • Django
        • 자연어처리
      • Spring
        • JPA
        • MSA
      • ETC
        • ERROR
      • 개발 일기 N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 인기 글

  • 태그

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
오도형석
[개발 일기] 2025.02.16 - Transaction Synchronization
상단으로

티스토리툴바