Spring Transaction

application_structure.png

어플리케이션은 위와 같이 3개의 계층으로 이루어져있다.

  1. presentation
    • controller
    • request 처리 Servlet, Spring MVC
  2. Service
    • service
    • 비즈니스 요청 처리
    • 특정 기술에 의존적이지 않은 순수 자바 코드로 작성
  3. DAL(Data Access Layer)
    • Repository
    • DB와 연동해서 직접적으로 데이터를 처리하는 계층
    • JDBC, JPA, etc

위를 통해서 알듯이, 서비스 계층은 특정 기술에 의존적이지 않은 순수 자바 코드를 이용해서 작성해야한다. 그렇게 해야, 기술을 변경하게 되더라도, 서비스 계층의 코드를 수정할 필요가 없게 된다. 즉 특정 구현체가 아닌 인터페이스에 의존하는 DIP 설계 원칙에 따라 서비스 계층을 설계해야한다.

아래의 MemberService class이 가지는 문제점들을 알아보자.

Member Service

private final DataSource dataSource;
private final MemberRepositoryV2 memberRepositoryV2;

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    Connection conn = dataSource.getConnection();

    try {
        conn.setAutoCommit(false);
        bizlogic(conn,fromId, toId, money);
        conn.commit();
    } catch (Exception ex) {
        log.error("Transfer Failed");
        conn.rollback();
        throw new IllegalStateException(ex);
    }
    finally{
        release(conn);
    }
}
  1. 트랜잭션 문제
    • 특정 기술 코드의 누수: 서비스 계층에 트랜잭션을 적용하게 되면서 DataSource, Connection, SQLException 과 같은 jdbc와 연관된 라이브러리를 사용하고 있다.
    • 트랜잭션 동기화 문제: 같은 트랜잭션을 유지하기 위해 파라미터로 커넥션으로 넘겨줘야했다.
  2. 예외 누수 문제 repository에서 발생한 SQLException 예외가 서비스 계층으로 넘어온다. 이는, JDBC 의존적인 에러로, JPA 와 같은 다른 DA로 변경하게 되면 서비스 계층의 코드를 수정해야한다.

  3. JDBC 반복 문제 service 계층에서 Try-Catch-Finally 구문이 반복되어서 사용되는 문제가 발생한다.

즉, 트랜잭션을 적용함에 따라 서비스 계층에 기술 의존적인 코드들이 많이 추가 되었다. 이러한 문제들에 대해서 Spring에서 여러 Interface, Class을 이용해서 문제를 해결하고 있다.

Transaction Abstraction

JDBC

Connection con = dataSource.getConnection();
con.setAutoCommit(false); //트랜잭션 시작
con.commit(); //성공시 커밋
con.rollback(); //실패시 롤백

JPA

EntityManagerFactory emf=Persistence.createEntityManagerFactory("jpabook");
EntityManager em=emf.createEntityManager();
EntityTransaction tx=em.getTransaction();
tx.begin(); //트랜잭션 시작
tx.commit(); //성공시 커밋 
tx.rollback(); //실패시 롤백

위와 같이 각 DA 기술 별로 트랜잭션을 시작하는 방식이 상이하다. 따라서, 특정 DA로 변경하고자하면 트랜잭션 관련 코드를 모두 수정해야하는 문제가 발생한다. 이를 인터페이스를 통해 추상화 시켜, 인터페이스를 이용해서 트랜잭션을 관리하도록 한다.

platform_transaction_manager

스프링에서는 위와 같이 PlatformTransactionManager 인터페이스를 이용해서 대부분의 DA에 대한 트랜잭션 접근 방식을 추상화 시켜놓았다.

PlatformTransactionManager

public interface PlatformTransactionManager extends TransactionManager {
  TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
  throws TransactionException;

  void commit(TransactionStatus status) throws TransactionException;

  void rollback(TransactionStatus status) throws TransactionException;
}

트랜잭션을 받아오는 메소드의 이름이 getTransaction 인데, 이는 이미 존재하는 트랜잭션이 있는 경우, 해당 트랜잭션을 받아올 수 있도록 설정할 수 있기 때문이다.(트랜잭션 전파)

Transaction Synchronization

트랜잭션을 이용하려면, 트랜잭션 내에서는 같은 커넥션-세션을 통한 DB 접근을 해야한다. 따라서, 트랜잭션을 동기화 시켜주는 작업이 필요하다.

기존에는 파라미터로 전달하는 방식을 활용했는데, 이렇게 되면 중복해서 메소드를 생성해야하는 문제가 존재했다. Spring에서는 쓰레드 동기화 매니저를 이용해서 트랜잭션을 동기화 시켜준다.

thread_synchronizer_manager

트랜잭션 매니저 내부에서는 트랜잭션 동기화 매니저를 사용하게 된다. 이때 트랜잭션 동기화 매니저는 쓰레드 로컬을 활용해서 멀티 쓰레드 상황에서 안전하게 커넥션을 동기화 할 수 있도록 한다. 트랜잭션 동기화 매니저를 통해 커넥션을 관리하게 되므로, 커넥션이 필요한 경우, 트랜잭션 동기롸 매니저로 부터 커넥션을 획득하면 된다.

동작 원리

  1. 트랜잭션 매니저는 Datasource를 이용해서 커넥션을 생성하고 트랜잭션을 만든다.
  2. 생선한 커넥션을 트랜잭션 동기화 매니져에 보관한다.
  3. Repository에서 커넥션이 필요한 경우, Datasource를 통해서 커넥션을 만들어내는 것이 아니라, 트랜잭션 매니저를 통해 커넥션을 할당받는다.
  4. 트랜잭션이 종료되면 트랜잭션 매니저에 커넥션을 반환한다.

이와 같이 트랜잭션 동기화 매니저를 사용하게 되면서, 파라미터를 통해 커넥션을 넘겨줄 필요가 없게 되었다.

Member Repository

//커넥션을 반환하는 메소드
private void close(Connection conn, Statement statement, ResultSet resultSet){
    JdbcUtils.closeResultSet(resultSet);
    JdbcUtils.closeStatement(statement);
    DataSourceUtils.releaseConnection(conn, dataSource);
}
//컨넥션을 할당받는 메소드
private Connection getConnection() throws SQLException {
    Connection connection = DataSourceUtils.getConnection(dataSource);
    log.info("connection= {} , {}", connection,connection.getClass());
    return connection;
}

트랜잭션 동기화 메소드를 이용하게 되면서, 더 이상 Datasource를 통해서 직접 커넥션을 생성하는 방식이 아닌, DatasourceUtils 메소드를 통해서 커넥션을 할당받아야한다. DatasourceUtils을 이용하게 되면 트랜잭션 동기화 매니저로부터 커넥션을 할당받을 수 있게 된다. 만약, 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우, 새로운 커넥션을 생성한다.

또한, 커넥션을 반환할 때에도, close를 통해서가 아닌, DataSourceUtils을 통한 해제를 수행해야한다. 트랜잭션이 유지되는 동안 커넥션을 유지해야하므로, DataSourceUtils를 통해 해당 커넥션은 트랜잭션 동기화 매니저에 반납하도록 해야한다. 트랜잭션 동기화 매니저를 통해 관리되는 커넥션이 아닌 경우 바로 반환하게 된다.

Member Service

private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 memberRepositoryV3;

public void accountTransfer(String fromId, String toId, int money) throws SQLException {

    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        bizlogic(fromId, toId, money);
        transactionManager.commit(status);

    } catch (Exception ex) {
        log.error("Transfer Failed");
        transactionManager.rollback(status);
        throw new IllegalStateException(ex);
    }
}

PlatformTransactionManager을 이용해서 Transaction 관리를 수행하도록 한다. 이때, PlatformTransactionManager은 인터페이스로, 구현 클래스를 외부에서 주입받아한다.(DI)

getTransaction 메소드를 통해서 트랜잭션을 시작하게 되고, TransactionStatus 객체를 반환받게 되는데, 이는 트랜잭션의 상태를 표현하는 클래스로 commit, rollback에서도 쓰이게 된다.

DefaultTransactionDefinition을 이용해서 트랜잭션과 관련된 옵션을 지정할 수 있다.

DI

DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,
USERNAME, PASSWORD);
PlatformTransactionManager transactionManager = new
DataSourceTransactionManager(dataSource);
memberRepository = new MemberRepositoryV3(dataSource);
memberService = new MemberServiceV3_1(transactionManager,
memberRepository);

현재는 JDBC 라이브러리를 통한 트랜잭션을 관리하므로 DataSourceTransactionManager 객체를 이용해서 PlatformTransactionManager을 구현하도록 한다.

Transcation Manager Mechanism

transaction_manager1

  1. 서비스 계층에서 PlatformTransactionManager을 통해 트랜잭션을 생성한다. 2~3. 커넥션을 생성하고 이를 수동 커밋 모드로 설정한다.
  2. 해당 커넥션을 트랜잭션 동기화 매니저에 저장한다.
  3. 트랜잭션 동기화 매니저를 이용해서 멀티쓰레드에 안전한 커넥션을 보관할 수 있다.

transaction_manager2

  1. 비즈니스 로직 중간에 리포지토리 메소드를 호출한다.
  2. repository에서 커넥션을 필요로 하는 경우, 트랜잭션 동기화 매니저로 부터 커넥션을 받아온다. DataSourceUtils
  3. 커넥션을 이용해서 필요한 데이터를 조회,수정, 등록, 등의 작업을 처리한다.

transaction_manager3

  1. 트랜잭션을 커밋, 롤백 하게 되면 트랜잭션이 종료된다.
  2. 트랜잭션을 커밋하기 위해, 동기화된 커넥션을 받아온다.
  3. 커넥션을 이용해서 커밋 또는 롤백을 처리한다.
  4. 리소스를 정리한다.
    • 트래잭션 동기화 매니저 정리
    • 커넥션의 수동 커밋모드를 해제한다.
    • 커넥션을 종료한다.

이와 같이 PlatformTransactionManager와 Transaction Synchronization Manager을 통해 특정 기술에 의존하지 않고 트랜잭션을 관리할 수 있게 되었다.

Transaction Template

//트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new
DefaultTransactionDefinition());
try {
    //비즈니스 로직
    bizLogic(fromId, toId, money);
    transactionManager.commit(status); //성공시 커밋
} catch (Exception e) {
    transactionManager.rollback(status); //실패시 롤백
    throw new IllegalStateException(e);
}

트랜잭션을 이용하게 되면 위와 같이 try-catch-finally 구문이 반복적으로 사용된다. 따라서, 이러한 코드의 반복을 줄이기 위해 template을 활용한다.

Transaction Template

public class TransactionTemplate {
    private PlatformTransactionManager transactionManager;
    //응답이 있는 경우 대해서, 사용
    public <T> T execute(TransactionCallback<T> action){..}
    //응답이 없는 경우, 사용
    void executeWithoutResult(Consumer<TransactionStatus> action){..}
}

Member Service

private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 memberRepositoryV3;

public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 memberRepositoryV3) {
    this.txTemplate = new TransactionTemplate(transactionManager);
    this.memberRepositoryV3 = memberRepositoryV3;
}

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    txTemplate.executeWithoutResult((status)->{
        try {
            bizlogic(fromId, toId, money);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    });
}

위와 같이, TransactionTemplate 객체를 할당해서, TransactionTemplate의 executeWithoutResult 메소드를 이용해서 비즈니스 로직을 실행하도록 한다.

이때, 비즈니스 로직에 대해서는 람다를 통해서 다음과 같이 수행한다.

txTemplate.executeWithoutResult((status)->{
    try {
        bizlogic(fromId, toId, money);
    } catch (SQLException e) {
        throw new IllegalStateException(e);
    }
});
  • 비즈니스 로직이 정상적으로 수행되면 커밋된다
  • 언체크 예외가 발생하게 되면 롤백된다.

다만, 위의 콜백 패턴에서는 SQLException과 같은 체크 오류에 대해서는 오류 전가를 할 수 없어서 해당 람다 안에서 try-catch를 이용해서 예외를 처리해줘야하는 문제가 있다.

TransactionTemplate를 이용해서, 트랜잭션을 처리하는 반복되는 코드의 양을 줄일 수 있게 되었다. 하지만, 비즈니스 로직 뿐만 아니라, 트랜잭션과 관련된 부분도 존재하는 문제는 여전히 존재한다. 이는 비즈니스를 처리하는 로직과 트랜잭션을 처리하는 기술이 한 곳에 있는, 즉 관심사가 2개가 존재하는 형태이다.

이를 궁극적으로, 해결하기 위해, Spring에서는 Transaction AOP라는 기술을 제공한다. AOP를 이용해서 서비스 계층에는 순수 자바 코드로만 이루어진 비즈니스 로직만이 존재하게 된다.

Transaction AOP

@Transactional annotation 하나만 명시해주게 되면 해당 서비스 계층에서는 트랜잭션을 이용할 수 있게 된다.

transaction_aop

트랜잭션 AOP를 활용하게 되면 위와 같이, 서비스 계층을 처리하기 전에 Transaction AOP와 같은 프록시가 삽입되여 트랜잭션 관련 작업을 처리하게 된다. 그리고 프록시에서 비즈니스 로직을 호출하는 부분이 존재하게 된다.

이렇게 프록시를 대입하게 되므로써, 트랜잭션을 처리하는 부분과 비즈니스 로직을 처리하는 부분을 완전히 분리할 수 있게 되었다.

Member Service

@Transactional
public void accountTransfer(String fromId, String toId, int money) throws
SQLException {
    bizLogic(fromId, toId, money);
}

위와 같이 @Transactional annotation 명시만으로, Transaction AOP를 활요할 수 있게 된다.

@Transaction annotation은 클래스 단위로 설정할 수 있고, 메소드 단위로도 설정할 수 있다.

MemberService Test

트랜잭션 AOP을 이용하기 위해서는 Spring Container을 이용해야한다. 트랜잭션 AOP는 TransactionManager와 DataSource를 주입받아야하는데, 내부적으로 Spring Container에서 해당 객체들을 조회하게 된다. 따라서, 트랜잭션 AOP를 이용하기 위해선 Spring Container가 존재해야하며, TransactionManager, DataSource와 같은 클래스에 대해서는 Spring Bean으로 등록해줘야한다.

@TestConfiguration
static class TestConfig {
    @Bean
    DataSource dataSource() {
        return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    }
    @Bean
    PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
    @Bean
    MemberRepositoryV3 memberRepositoryV3() {
        return new MemberRepositoryV3(dataSource());
    }
    @Bean
    MemberServiceV3_3 memberServiceV3_3() {
        return new MemberServiceV3_3(memberRepositoryV3()); 
    }
}

위와 같이 필요한 객체에 대해서 Bean으로 관리해서, 트랜잭션 AOP를 활용할 수 있도록 한다.

Spring Transaction Total Mechanism

transaction_aop_mechanism

기존의 트랜잭션 처리 과정에서 AOP 프록시만 추가된다.

이때, 프록시 객체에 필요한 객체(DataSource, TransactionManager)들은 Spring Contrainer 내부에 등록된 Spring Bean을 통해 DI를 받게 된다.

나머지 트랜잭션 처리 과정은 위에서 언급한 것과 동일하게 동작한다.

Auto Resource Registration

지금까지는 DataSource와 Transaction Manager을 직접 Spring Bean으로 등록해서 DI를 진행했다. 하지만, Spring은 위 2개의 클래스에 대해서, 자동으로 등록시켜준다.

만일, 개발자가 직접, 위 2개의 클래스에 대해, 직접 Spring Bean으로 생성/등록하는 경우, Spring에서는 따로 추가적으로 Spring Bean으로 등록하지 않는다.

DataSource

application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=

위와 같이, DataSource에 관련된 속성 정보를 명시해놓으면, Spring은 위 정보를 토대로, DataSource를 생성해서 Spring Bean으로 등록한다.

기본 설정은 HikariDataSource이다.

TransactionManager

트랜잭션의 매니저의 경우, 현재 등록된 DA 기술 라이브러리를 토대로 결정된다. JDBC 관련 라이브러리를 활용하는 경우, DataSourceTransactionManager을 빈으로 등록하게 되고, JPA를 사용하고 있으면 JPATransactionManager을 등록하게 된다.

References

link: inflearn

link:springdb

댓글남기기