Spring/Spring DB

문제해결 - 트랜잭션 동기화 문제

YellowCow 2025. 7. 19. 20:52

트랜잭션은 동일한 Connection 안에서만 동일한 트랜잭션으로 간주된다

그래서 트랜잭션이 종료될 때까지 동일한 Connection을 유지해야 하므로

데이터 접근계층 로직 간에 같은 커넥션을 사용할 필요가 있다

 

아래의 코드를 보자

아래 코드의 경우에는 데이터 접근계층 로직 간에 같은 커넥션을 사용하기 위해

데이터 접근계층 로직을 호출할 때마다 Connection객체를 인자로 받도록 되어있다

이는 OCP 원칙에 위배되며 메서드의 확장이 어렵게 된다는 문제가 있다

public class MemberRepository{
    Member save(Connection connection, Member member) throws SQLException;

    Member findById(Connection connection, String memberId) throws SQLException;

    void update(Connection connection, String memberId, Integer money) throws SQLException;

    void delete(Connection connection, String memberId) throws SQLException;
}

 

 

이 문제를 해결하기 위해서 스프링에서는 TransactionSynchronizationManager라는 클래스를 제공한다

TransactionSynchronizationManager는 자바의 ThreadLocal이라는 개념을 이용하여 관리한다

ThreadLocal은 각 쓰레드 마다 저장공간을 제공하는 개념이다

TransactionSynchronizationManager는 동일한 쓰레드 내에서 Connection을 공유할 수 있게 해준다

덕분에, 데이터 접근계층 로직을 호출할 때마다 Connection 객체를 전달하지 않아도 된다

 


개념적으로 보면 아래 그림과 같다

1. 클라이언트가 요청한다

2. 서비스 계층에서 트랜잭션 매니저에게 트랜잭션을 요청한다

3. 트랜잭션 매니저는 트랜잭션 동기화 매니저에 보관된 Connection이 있는지 확인한다

3-1. 만약, 트랜잭션 동기화 매니저에 보관된 Connection이 없는 경우, 새 Connection을 얻어온다

4. 비즈니스 로직을 실행한다

5. 트랜잭션을 종료한다

6. 트랜잭션 동기화 매니저에 보관된 커넥션과 연결된 트랜잭션이 트랜잭션 매니저에 없는 경우, 커넥션을 종료한다

 

아래는 트랜잭션 동기화 매니저를 적용한 예제이다

아래 코드에서는 getConnection 메서드에서 DataSourceUtils클래스를 이용하여 ThreadLocal에서 Connection을 얻어와서 사용하도록 되어있다

private final DataSource dataSource;

public MemberRepositoryV3(DataSource dataSource) {
    this.dataSource = dataSource;
}

public Member save(Member member) throws SQLException {
    String sql = "INSERT INTO member (member_id, money) VALUES (?, ?)";

    Connection connection = null;
    PreparedStatement preparedStatement = null;

    try {
        connection = getConnection();
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, member.getMemberId());
        preparedStatement.setInt(2, member.getMoney());
        preparedStatement.executeUpdate();
        return member;
    } catch (SQLException e) {
        log.error("db error", e);
        throw e;
    } finally {
        close(connection, preparedStatement, null );
    }

}

public Member findById(String memberId) throws SQLException {
    String sql = "SELECT * FROM member WHERE member_id = ?";
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    try {
        connection = getConnection();
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, memberId);
        resultSet = preparedStatement.executeQuery();
        if (resultSet.next()) {
            Member member = new Member();
            member.setMemberId(resultSet.getString("member_id"));
            member.setMoney(resultSet.getInt("money"));
            return member;
        } else {
            throw new NoSuchElementException("member not found memberId=" + memberId);
        }
    } catch (SQLException e){
        log.error("db error, findById", e);
        throw e;
    } finally {
        close(connection, preparedStatement, resultSet);
    }
}

public void update(String memberId, Integer money) throws SQLException {
    String sql = "UPDATE member SET money = ? WHERE member_id = ?";

    Connection connection = null;
    PreparedStatement preparedStatement = null;

    try {
        connection = getConnection();
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, money);
        preparedStatement.setString(2, memberId);
        int size = preparedStatement.executeUpdate();
        log.info("update rows = {}", size);
    } catch (SQLException e) {
        log.error("db error", e);
        throw e;
    } finally {
        close(connection, preparedStatement, null );
    }

}

public void delete(String memberId) throws SQLException {
    String sql = "DELETE FROM member WHERE member_id = ?";

    Connection connection = null;
    PreparedStatement preparedStatement = null;

    try {
        connection = getConnection();
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, memberId);
        int size = preparedStatement.executeUpdate();
        log.info("deleted rows = {}", size);
    } catch (SQLException e) {
        log.error("db error", e);
        throw e;
    } finally {
        close(connection, preparedStatement, null );
    }

}

private void close(Connection connection, Statement statement, ResultSet resultSet) {
    JdbcUtils.closeResultSet(resultSet);
    JdbcUtils.closeStatement(statement);
    // 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다
    DataSourceUtils.releaseConnection(connection, dataSource);
}

// 트랜잭션 동기화를 위해 Connection 객체를 얻어오는 메서드
private Connection getConnection() throws SQLException {
    // 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다
    // TheadLocal에서 Connection을 꺼내온다
    Connection connection = DataSourceUtils.getConnection(dataSource);
    log.info("get connection = {}, class = {}", connection, connection.getClass());
    return connection;
}