[ Spring] Transaction Management : @Transactional

공호진·2022년 8월 27일
0

Spring Transaction

목록 보기
1/2
post-custom-banner

1. Intro

이 글은 해당 글을을 번역한 것입니다.


2. 기본 JDBC Transaction Management 작동 방식

어떻게 JDBC Transaction을 시작하고, 커밋 혹은 롤백을 할까?

Spring의 @Transactional annotation, 일반 Hibernate, jOOQ 또는 기타 데이터베이스 라이브러리를 사용하던지 간에, 결국 dababase transaction을 열고 닫기 위해 동일한 동작을 수행한다.

기본적인 JDBC Transaction은 다음과 같다.

import java.sql.Connection;

Connection connection = dataSource.getConnection(); // (1)

try (connection) {
    connection.setAutoCommit(false); // (2)
    // execute some SQL statements...
    connection.commit(); // (3)

} catch (SQLException e) {
    connection.rollback(); // (4)
}
  1. Transanction을 시작하려면, database 에 연결이 필요하다.
  2. 명칭은 약간 생소할 수 있지만, java에서 database transaction을 시작할 수 있는 유일한 방법이다.

    setAutoCommit(true) 는 모든 단일 SQL 문이 자동으로 자체 트랜잭션에 wrapping되도록 하고 setAutoCommit(false) 는 그 반대이다.
    : master transaction 내의 트랜잭션 작업 단위가 반복적으로 commit 메서드를 호출하지 않도록 하기 위해 false를 사용

  3. Transaction 커밋.
  4. 만약 exception이 발생한다면 rollback

위의 4 줄이 @Transaction annotation을 사용할 때마다, 스프링이 해주는 모든 것이다.(단순화 시켜서)


3. Spring 혹은 Spring Boot의 Transaction Management 작동 방식

JDBC의 작동원리를 바탕으로, Spring의 transaction을 알아보자.
일반 JDBC를 사용하면 트랜잭션을 Management하는 방법이 단 하나 setAutocommit(false)) 인 반면, Spring은 동일한 작업을 수행할 수 있는 여러 가지 더 편리한 방법을 제공합니다.

3.1. 프로그래밍 방식의 transaction management 사용

Spring에서 트랜잭션을 정의하는 방식이지만, 다소 드물게 사용되는 방법은 프로그래밍 방식으로, TransactionTemplate을 통하거나 직접 PlatformTransactionManager를 통하는 것이다.
코드로 보면 다음과 같다.

@Service
public class UserService {

    @Autowired
    private TransactionTemplate template;

    public Long registerUser(User user) {
        Long id = template.execute(status ->  {
            // execute some SQL that e.g.
            // inserts the user into the db and returns the autogenerated id
            return id;
        });
    }
}

일반 JDBC 와 비교했을 때,

  1. 데이터베이스 연결을 열거나 닫는 작업을 할 필요가 없다. 대신 Transaction Callbacks 를 사용해야 한다.
  2. 스프링은 exception이 발생했을 때, runtime exception으로 변환해주기 때문에, SQLException을 catch 할 필요가 없다.
  3. 스프링 생태계에 효과적으로 통합되기 때문에, 많은 Spring context의 configuration에서 필요하지만, 더 이상 추가적인 작업을 하지 않아도 된다.

3.2. Spring의 @Transactional 주석을 사용하는 방법(선언적 트랜잭션 관리)

다음은 Spring transaction management가 일반적으로 활용되는 방법이다.

public class UserService {

    @Transactional
    public Long registerUser(User user) {
       // execute some SQL that e.g.
        // inserts the user into the db and retrieves the autogenerated id
        // userDao.save(user);
        return id;
    }
}

더 이상 다른 구성이 필요하지 않지만, 다음과 같은 2 가지 작업이 필요하다.

  • Spring configuration으로 @EnableTransactionManagement annotation 사용이 필요하다.
    (spring boot는 해당 옵션을 자동처리)
  • Spring configuration에서 transaction manager(모든 트랜잭션 방식에서 필요)

그러면 @Transactional annotation을 추가한 모든 빈의 public method 에 database transaction이 실행된다.

즉 @Transactional을 사용하려면 다음과 같은 transaction manament를 작성해주면 된다.

@Configuration
@EnableTransactionManagement
public class MySpringConfig {

    @Bean
    public PlatformTransactionManager txManager() {
        return yourTxManager; // more on that later
    }

}

3.3. @Transaction의 원리(CGlib & JDK Proxies)

Spring에는 프록시를 다루기 위한 이점이 있는데, 그 핵심은 IoC 컨테이너이다. 컨테이너는 UserService를 인스턴스화하고 해당 UserService를 UserService가 필요한 다른 빈에 DI한다.

이제 빈에서 @Transactional을 사용할 때마다 Spring은 작은 트릭을 사용한다. UserService뿐만 아니라 해당 UserService의 트랜잭션 프록시 도 인스턴스화합니다 .

위의 과정은 Cglib 라이브러리의 도움으로 서브클래싱을 통한 프록시 수행된다. ( Dynamic JDK 프록시 와 같은) 프록시를 구성하는 다른 방법도 있음

위의 과정을 이미지로 풀면 다음과 같다.

  • 데이터베이스 연결/트랜잭션 열기 및 닫기.

  • 그런 다음 작성한 실제 UserService 에 위임

  • 그리고 UserRestController와 같은 다른 bean은 실제(target) 가 아니라 프록시와 호출하고 있다는 것을 알지 못한다.

3.4. 물리적 트랜잭션과 논리적 트랜잭션

다음 두 트랜잭션 클래스가 있다고 가정하자.

@Service
public class UserService {

    @Autowired
    private InvoiceService invoiceService;

    @Transactional
    public void invoice() {
        invoiceService.createPdf();
        // send invoice as email, etc.
    }
}

@Service
public class InvoiceService {

    @Transactional
    public void createPdf() {
        // ...
    }
}

UserService에는 invoice() 메서드가 있고
InvoiceService에서 다른 트랜잭션 메서드인 createPdf()를 호출한다.

데이터베이스 트랜잭션의 관점에서 이 작업은 실제로 하나의 데이터베이스 트랜잭션이어야 한다. (위의 개념을 기억하자: getConnection(). setAutocommit(false). commit(). )
Spring은 이 물리적 트랜잭션을 호출한다.

그러나 Spring에서는 두 가지 논리적 트랜잭션이 발생한다. 첫 번째는 UserService에서, 다른 하나는 InvoiceService에서 발생한다. Spring은 두 @Transactional 메소드가 동일한 기본 물리적 데이터베이스 트랜잭션을 사용해야 한다는 것을 알 만큼 영리하다.

InvoiceService가 다음과 같이 변경되면 상황이 어떻게 달라질까?

@Service
public class InvoiceService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
        // ...
    }
}

propagation(전파) 모드를 require_new로 변경하면 이미 존재하는 다른 트랜잭션과 독립적으로 createPDF()가 자체 트랜잭션에서 실행되어야 한다고 Spring에 알린다.

위의 코드는 데이터베이스에 대한 두 개의 (물리적) connections/transactions을 열 것임을 의미한다. (즉: getConnection() x2. setAutocommit(false) x2. commit() x2 ) 이제 Spring은 두 개의 논리적 트랜잭션 조각(invoice()/createPdf())이 이제 두 개의 서로 다른 물리적 데이터베이스에 매핑된다.

요약하자면

물리적 트랜잭션: 실제 JDBC 트랜잭션
논리적 트랜잭션: (잠재적으로 중첩된) @Transactional-annotated(Spring) 메서드

3.4.1. @Transactional 주의할 점

Spirng을 처음 시작하는 사람들이 일반적으로 겪는 한 가지 이슈이다. 다음과 같은 코드가 있다고 가정하자.

@Service
public class UserService {

    @Transactional
    public void invoice() {
        createPdf();
        // send invoice as email, etc.
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createPdf() {
        // ...
    }
}

invoice()에서 트랜잭션이기도 한 createPDF()를 호출했을 때
누군가가 invoke()를 호출하면 실제 트랜잭션이 몇 개나 열릴까?
답은 한 개이다.

왜냐하면, Spring은 트랜잭션 UserService 프록시를 생성하지만 일단 UserService 클래스 내부에 있고 다른 내부 메소드를 호출하면 더 이상 프록시가 관련되지 않는다. 즉, 새로운 transaction이 생성되지 않는다.

그림으로 보면 다음과 같다.


4. Spring and JPA / Hibernate Transaction Management 작동 방식

먼저 순수 Hibernate의 사용법을 보면 다음과 같다.

public class UserService {

    @Autowired
    private SessionFactory sessionFactory; // (1)

    public void registerUser(User user) {

        Session session = sessionFactory.openSession(); // (2)

        // lets open up a transaction. remember setAutocommit(false)!
        session.beginTransaction();

        // save == insert our objects
        session.save(user);

        // and commit it
        session.getTransaction().commit();

        // close the session == our jdbc connection
        session.close();
    }
}
  1. 모든 Hibernate 쿼리에 대한 진입점인(entry point) Hibernate SessionFactory
  2. Hibernate의 API를 사용하여 세션(읽기: 데이터베이스 연결) 및 트랜잭션을 수동으로 관리합니다.

그러나 위의 코드에는 한 가지 큰 문제가 있다.

  • Hibernate는 Spring의 @Transactional annotation 대해 알지 못한다.
  • Spring의 @Transactional은 Hibernate의 트랜잭션에 대해 아무것도 알지 못한다.

그러나 @Transactional 을 사용함으로써, spring과 hibernate를 통합할 수 있다. 즉, 서로의 트랜잭션에 대해 알 수 있다.

코드를 보면

@Service
public class UserService {

    @Autowired
    private SessionFactory sessionFactory; // (1)

    @Transactional
    public void registerUser(User user) {
        sessionFactory.getCurrentSession().save(user); // (2)
    }
}
  1. 이전과 동일한 SessionFactory
  2. getCurrentSession() 및 @Transactional이 동기화 되기 때문에, 더 이상 수동 상태 관리는 필요하지 않다.

이러한 통합 절차는 Spring 구성에서 DataSourcePlatformTransactionManager 를 사용하는 대신에 HibernateTransactionManager (일반 Hibernate를 사용하는 경우) 또는 JpaTransactionManager (JPA를 통해 Hibernate를 사용하는 경우)를 사용함으로써 가능하다.

그림으로 단순화해서 표현할 경우, 다음과 같은 방법으로 통합된다.

원문 : https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth#spring-section

profile
내일 더 나은 개발자가 되기 위해, 오늘을 기록합니다
post-custom-banner

0개의 댓글