Domain/Spring

[Spring] JDBC와 Transaction

by Donghwan 2021. 8. 18.

트랜잭션(Transaction)은 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미합니다.

DB는 그 자체로 완벽한 트랜잭션을 지원합니다. SQL을 이용해 다중 로우의 수정이나 삭제를 위한 요청을 했을 때 일부 로우만 삭제되고 나머지는 안 된다거나, 일부 필드는 수정했는데 나머지 필드는 수정이 안 되고 실패로 끝나는 경우는 없습니다. 하나의 SQL 명령 을 처리하는 경우는 DB가 트랜잭션을 보장해준다고 믿을 수 있습니다. 하지만 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야하는 경우도 있습니다.

문제는 선행 SQL을 성공적으로 실행했지만 후행 SQL이 성공하기 전에 장애가 생겨서 작업이 중단되는 경우입니다. 이 때, 여러 SQL이 하나의 트랜잭션이 되려면 후행 SQL이 성공적으로 수행되기 전에 문제가 발생할 경우 선행 SQL의 작업도 취소 시켜야 합니다. 이런 취소 작업을 트랜잭션 롤백이라고 합니다. 반대로 성공적으로 처리되어 DB에 작업을 확정하는 것을 트랜잭션 커밋이라고 합니다.

 

@트랜잭션 경계

모든 트랜잭션은 시작하는 지점과 끝나는 지점이 있습니다. 트랜잭션이 시작되고 끝나는 위치를 트랜잭션 경계라고 합니다. 시작하는 방법은 한가지이지만 끝나는 방법은 롤백 또는 커밋 두가지입니다. 정확한 트랜잭션 경계를 설정하는 것은 매우 중요합니다.



@JDBC 트랜잭션 경계 ( 메소드 내 트랜잭션)

JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어납니다. 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄지기 때문입니다. JDBC에서 트랜잭션을 시작하려면 자동커밋 옵션을 false로 만들어주면 됩니다. JDBC의 기본설정은 DB 작업을 수행한 직 후 자동으로 커밋이 되도록 되어 있습니다. 자동커밋 옵션을 false로 하지 않으면 작업마다 커밋해서 트랜잭션을 끝내버리게 됩니다.

하지만 JDBC에서는 이 기능을 false로 설정해주면 새로운 트랜잭션이 시작하도록 만들 수 있습니다. 트랜잭션이 한 번 시작되면 commit() 또는 rollback() 메소드가 호출될 때까지의 작업이 하나의 트랜잭션으로 묶입니다. commit() 또는 rollback()이 호출되면 그에 따라 작업 결과가 DB에 반영되거나 취소되고 트랜잭션이 종료됩니다.

트랜잭션의 경계는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재합니다. 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션이라고도 합니다. 따라서 템플릿 메소드가 호출될 때마다 트랜잭션이 새로 만들어지고 메소드를 빠져나오기 전에 종료됩니다. JdbcTemplate의 메소드를 사용하는경우는 각 메소드마다 하나씩의 독립적인 트랜잭션으로 실행될 수 밖에 없습니다.

//대략적인 예시
public void sample() throws SQLException {
    Connection conn = dataSource.getConnection();

    conn.setAutoCommit(false); //트랜잭션의 시작
    try {
        PreparedStatement preparedStatement1 = conn.prepareStatement("sql");
        preparedStatement1.executeUpdate();

        PreparedStatement preparedStatement2 = conn.prepareStatement("sql");
        preparedStatement2.executeUpdate();

        conn.commit();
    } catch (Exception e) {
        conn.rollback();
    } finally {
        conn.close();
    }
}

 

@비즈니스 로직 내 트랜잭션

public void update() {
    updateDB1();
    updateDB2();
    updateDB3();
}

각 메소드마다 하나씩의 독립적인 트랜잭션으로 실행된다면 비즈니스 로직(DAO, Service) 내에서 update와 같은 메서드처럼 여러 번 DB 업데이트를 하는 경우에 동일한 문제가 발생할 수 있습니다. updateDB1에 대한 결과를 커밋하였더라도 updateDB2, updateDB3에서 오류가 발생한 경우에도 위에서 본 사례처럼 트랜잭션 문제가 발생할 수 있습니다. 

JDBC API를 직접 사용하거나 JdbcTemplate를 사용하면 매번 DB 커넥션을  여러 가지 작업을 하나의 트랜잭션으로 묶는 일이 불가능해집니다. 어떤 일련의 작업이 하나의 트랜잭션으로 묶이려면 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용해야 합니다.

 

@트랜잭션 동기화

비즈니스 로직을 담고 있는 UserService 메소드 안에서 트랜잭션의 경계를 설정해 관리하려면 지금까지 만들었던 깔끔하게 정리된 코드를 포기해야 할까? 아니면 트랜잭 션 기능을 포기해야 할까? 스프링은 이 딜레마를 해결할 수 있는 멋진 방법을 제공해줍니다.

public void sample() throws SQLException {
    //트랜잭션 동기화 관리자를 이용해 동기화 작업을 초기화합니다.
    TransactionSynchronizationManager.initSynchronization();
    //DB 커넥션을 생성하고 트랜잭션을 시작합니다. 이후의 이 클래스의 작업은 모두 여기서 시작한 트랜잭션 안에서 진행됩니다.
    Connection conn = DataSourceUtils.getConnection(dataSource); //DB 커넥션 생성과 동기화를 함께 해주는 유틸리티 메소드입니다.
    conn.setAutoCommit(false);
    
    try {
        PreparedStatement preparedStatement1 = conn.prepareStatement("sql");
        preparedStatement1.executeUpdate();
        
        PreparedStatement preparedStatement2 = conn.prepareStatement("sql");
        preparedStatement2.executeUpdate();
        
        conn.commit();
    } catch (Exception e) {
        conn.rollback();
    } finally {
        DataSourceUtils.releaseConnection(conn, dataSource); //스프링 유틸리티 메소드를 통해 Connection을 안전하게 닫습니다.
        //동기화 작업을 종료 및 정리합니다.
        TransactionSynchronizationManager.unbindResource(this.dataSource);
        TransactionSynchronizationManager.clearSynchronization();
    }
}

DataSourceUtils의 getConnection() 메소드는 Connection 오 브젝트를 생성해줄 뿐만 아니라 트랜잭션 동기화에 사용하도록 저장소에 바인딩해주기 때문이다. 트랜잭션 동기화가 되어 있는 채로〕dbcTemplate을 사용하 면〕dbcTemplate의 작업에서 동기화시킨 DB 커넥션을 사용하게 된다. 스프링 유틸리티 메소드의 도움을 받아 커넥션을 닫고 트랜잭션 동기화를 마치도록 요청하면 된다.

 

@JdbcTemplate와 트랜잭션 동기화

JdbcTemplate은 영리하게 동작하도록 설계되어 있습니다. 만약 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB 커넥션을 만들고 트랜잭션을 시작해서 JDBC 작업을 진행합니다. 트랜잭션이 굳이 필요 없다면 바로 호출해서 사용해도 되고, 외부에서 트랜잭션을 만들고 이를 관리할 필요가 있다면 미리 DB 커넥션을 생성한 다음 트랜잭션 동기화를 해주고 사용하면 됩니다. JDBC 코드의 try/catch/finally 작업 흐름 지원, SQLException의 예외 변환과 함께 JdbcTemplate이 제 공해주는 세 가지 유용한 기능 중 하나입니다.

 

@글로벌 트랜잭션

하나의 트랜잭션 안에서 여러 개의 DB에 데이터를 넣는 작업을 해야 할 필요가 발생할 경우 한 개 이상의 DB로의 작업을 하나의 트랜잭션으로 만드는 건 JDBC의 Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 불가능합니다. 왜냐하면 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문입니다. 따라서 각 DB와 독립적으로 만들어지는 Connection을 통해서가 아니라, 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션 방식을 사용해야 합니다.

문제는 JDBC, JTA, 하이버네이트, JPA, JDO 등에서 다양한 글로벌 트랜잭션 API들의 의존관계와 추상화에 있습니다. 다행스럽게도 스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있습니다. 이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API를 이용하지 않고도, 일관된 방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해집니다.

 

@PlatformTransactionManager

스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스입니다.

public void sample() { 
    PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    
    try {
        //...
        transactionManager.commit(status);
    } catch (Exception e) {
        transactionManager.rollback(status);
        throw e;
    }
}

JDBC의 로컬 트랜잭션을 이용한다면 PlatformTransactionManager를 구현한 DataSourceTransactionManager를 사용하면 됩니다. 사용할 DB의 DataSource를 생성자 파라미터로 넣으면서 DataSourceTransactionManager의 오브젝트를 만듭니다.

JDBC를 이용하는 경우에는 먼저 Connection을 생성하고 나서 트랜잭션을 시작했지만 PlatformTransactionManager에서는 트랜잭션을 가져오는 요청인 getTransaction() 메소드를 호출하기만 하면 됩니다. 필요에 따라 트랜잭션 매니저 가 DB 커넥션을 가져오는 작업도 같이 수행해주기 때문입니다.

트랜잭션을 가져온다는 의미는 일단 트랜잭션을 시작한다는 의미라고 생각하겠습니다. 이 때, 파라미터로 넘기는 DefaultTransactionDefinition 오브젝트는 트랜잭션에 대한 속성을 담고 있습니다. 이렇게 시작된 트랜잭션은 Transactionstatus 타입의 변수에 저장됩니다. Transactionstatus는 트랜잭션에 대한 조작이 필요할 때 PlatformTransactionManager 메소드의 파라미터로 전달해주면 됩니다.

스프링의 트랜잭션 추상화 기술은 앞에서 적용해봤던 트랜잭션 동기화를 사용합니다. PlatformTransactionManager로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장됩니다. PlatformTransactionManager를 구현한 DataSourceTransactionManager 오브젝트는 JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리해줍니다.

 

@PlatformTransactionManager와 DI

PlatformTransactionManager를 빈으로 등록하여 DI로 주입받는다면 JDBC, JTA, Hibernate 등등 모두에서 코드 변경없이 사용이 가능합니다. 

 


출처

728x90
반응형

댓글