BackEnd/Spring

[Spring] Transaction 동작 원리 (@Transactional 원리)

샤아이인 2022. 3. 9.

이 글의 최종 목적은 @Transactional 이 어떻게 돌아가는지를 알아보는 것 입니다.

하지만 우선적으로 JDBC에서 트랜잭션을 어떠한 방식으로 적용시키는지를 이해할 필요가 있습니다.

 

1. JDBC Transaction 이해하기

우선 대략적인 JDBC의 트랜잭션을 다루는 코드는 다음과 같습니다.

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. 우선적으로 DB와 연결부터 해야합니다. (data-source 설정했고 data-source를 통해서 Connection을 가져옵니다)
  2. Java에서 데이터베이스의 트랜잭션을 시작하는 유일한 방법입니다. setAutoCommit(true)는 모든 SQL statement를 래핑합니다. 즉, JDBC 라이브러리 룰에 따라 자동으로 커밋, 롤백 즉 트랜잭션이 이뤄집니다. setAutoCommit(false)는 이와 반대로 트랜잭션의 주인은 내가 됩니다. 즉 제어를 내가하고 내가 원할 때 커밋 또는 롤백합니다. 다만 commint의 begin()은 setAutoCommit(false) 호출과 동시에 시작됩니다.
  3. 명시적으로 commit을 합니다.
  4. 예외 발생시 rollback하게 됩니다.

그럼 왜 setAutoCommit(false) 을 사용하는 것 일까요?

true로 사용하면 사용자는 편하게 사용할수있는 것 아닐까요?

=> 수행하고자 하는 하나의 "작업 단위" 로 묶어두고싶기 때문입니다.

 

예를 들어 볼까요? A사용자가 계좌에서 10만원을 출금하여 B한테 송금한다고 해봅시다.

위와같이 작업A, 작업 B 총 2개의 작업단위로 나뉘게 됩니다.

 

순서대로 작업되던 도중 작업A 와 작업 B 사이에서 오류가 발생한다면 어떻게 될까요?

이미 작업 A는 처리되어 commit 된 시점이라, A의 돈 10만원은 차감되있는 상황입니다.

문제가 발생되었기 때문에 rollback을 하게 되는데, 가장 가까운 commit 시점인 작업 A 가 끝난 시점으로 돌아가게 됩니다.

이러면 A사용자는 돈 10만원을 날리게 된 것 이죠 ㅠ,ㅠ...

 

따라서 이러한 문제를 해결하기 위해 "작업 단위"를 직접 지정하게 되는 것 입니다. 다음 과 같이 말이죠!

auto-commit을 false로 해두었기 때문에 작업 A 를 진행하던 도중에 어디에서든 문제가 발생해도 맨 처음 "A계좌 조회" 전의 시작단계로 rollback하게 됩니다!! 

이전과 같은 상황이라면 A의 10만원 또한 복구될 것 입니다!

 

이것이 JDBC의 기본적인 트랜잭션 방법이자, Spring의 @Transactional 의 핵심입니다.

위에서 언급한대로 'Java에서 트랜잭션을 시작하는 유일한 방법'이기 때문에 Spring의 @Transactional 도 동일하게 동작합니다. 

 

2. JDBC isolation levels and savepoints

만약 Spring을 어느정도 사용해본 분 이라면 @Transactional 을 사용하면서 다음과 같은 코드를 본적이 있을 것 입니다.

@Transactional(propagation=TransactionDefinition.NESTED,
               isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED)

우선 propagation, isolation 옵션은 당장은 신경쓰지 맙시다!

다만 위와같이 옵션을 추가해주면 다음 JDBC 코드와 같이 해당 옵션들이 추가되게 됩니다.

import java.sql.Connection;

// isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // (1)

// propagation=TransactionDefinition.NESTED
Savepoint savePoint = connection.setSavepoint(); // (2)
...
connection.rollback(savePoint);

(1) 트랜잭션의 고립 레벨을 지정하는 방법이다.

(2) Spring에서의 NESTED 전파옵션의 트랜잭션은 JDBC/database의 savepoints로 동작합니다.

 

위에서 봤듯 JDBC를 이용한 Transaction isolation, propagation처리를 하는데, 스프링에서는 직접 JDBC와 같은 코드를 개발자가 사용하지않고 편리하게 사용할 수 있도록 다양한 방법을 제공합니다.

이또한 서비스 추상화(PSA)라 할수 있습니다.

 

3. Spring에서의 Transaction Management의 동작 방식

일단 순수하게 Spring Core에서 어떠한 방식으로 처리하는지 부터 알아봅시다.

Transaction의 management는 어떤 의미 일까요? 스프링은 JDBC 트랜잭션을 어떻게 시작하고, commit 또는 rollback을 할까요?

위에서 살짝 언급한적이 있는데? 기억 하시나요?

 

위에서는 순수 JDBC를 이용한 setAutocommit(false) 기능을 설명했지만, Spring은 좀더 편리한 다양한 방식을 제공하고 있습니다.

그러한 편리한 기능 중에서 우선 자주사용하지 않는 방식부터 살펴볼까요?

@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 예제와 비교해 봅시다.

  • 데이터베이스 커넥션(Connection)을 try-catch를 사용하면서 복잡하게 직접 열고 닫을 필요가 없습니다. 대신에 트랜잭션 콜백 메서드를 사용합니다.
  • SQLExceptions를 잡을 필요가 없습니다. Spring이 알아서 RuntimeException으로 변환해줍니다.
  • Spring 환경에 더 적절하고, TransactionTemplate은 내부적으로 TransactionManager를 사용합니다. 이 모든 것이 Spring context configuration에서 지정해줘야하는 Bean이지만, 나중에 수정할 필요가 없습니다.

위와 같은 프로그래밍적인 transaction 관리는 잘 사용하지 않습니다. 다만 선언적으로 Transaction을 관리한다는점이 핵심 입니다.

 

추가적으로 XML을 통한 transaction의 관리방법도 있지만 이는 생략하도록 하겠습니다.

 

우리는 사실상 대부분의 경우에서 @Transaction을 사용하게 될 것 입니다!

 

4. Spring은 어떤 방식으로 @Transaction을 사용할까요?

다음은 최근 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;
    }
}

어떻게 이런 간단한 방식으로 처리되는 것 일까요? 우리가 @Transactional 을 붙인다면

 

  • Spring Configuration에 @EnableTransactionManagement 애노테이션을 붙입니다. (스프링 부트에서는 자동으로 해줍니다.)
  • Spring Configuration에서 트랜잭션 매니저를 지정합니다. 

이렇게만 하면 스프링은 Transaction을 처리해줄정도로 매우 영리합니다.

@Transactional 애노테이션이 달린 public 메서드에 대해서 내부적으로 데이터베이스 트랜잭션 처리를 해줍니다.

 

따라서 @Transactional 이 적용되면 코드는 다음과 같아집니다.

@Configuration
@EnableTransactionManagement
public class MySpringConfig {

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

}

따라서 

@Transactional이 쓰인 UserService코드를 간단히 변환하면 아래와 같습니다.

public class UserService {

    public Long registerUser(User user) {
        Connection connection = dataSource.getConnection(); // (1)
        try (connection) {
            connection.setAutoCommit(false); // (1)

            // execute some SQL that e.g.
            // inserts the user into the db and retrieves the autogenerated id
            // userDao.save(user); <(2)

            connection.commit(); // (1)
        } catch (SQLException e) {
            connection.rollback(); // (1)
        }
    }
}

 

(1) 그냥 @Transactional 만 추가해주면 JDBC에서 필요한 코드를 알아서 자동 삽입해줍니다.

Connection도 가져오고, setAutoCommit(false)해주고 추가해주고, 해당 메소드가 끝나면 commit, 예외 발생하면 rollback까지!

 

(2) 내가 작성한 userDao에 user를 저장하는 코드 부분

 

그렇다면 Spring은 이런 코드를 어떻게 추가해주는 것 일까요? 다음 단락을 통해 알아보시죠!

 

5. CGlib & JDK Proxies - @Transactional 의 이면에 숨겨진 기술들

스프링이 실제로 내가 작성한 자바 코드에 추가로 재 작성할 수는 없습니다 (바이트 코드 위빙같은 고급기술은 여기서는 무시합시다...)

위에서 보여준 registerUser() 메서드를 호출하면 userDao.save(user)을 호출하는 것은 바뀌지 않는 사실입니다.

 

대신에 Spring은 IoC Container라는 장점을 활용합니다.

우리가 @Transactional을 사용하면 UserSerice를 초기화 할 뿐만 아니라, UserServiceProxy 또한 초기화 하는 것 입니다.

CGlib 라이브러리의 도움을 받아 만든 proxy를 사용하면 마치 실제 userSerivce코드에 위에서 보여준 Transaction 코드를 추가하여 사용하는 것 처럼 동작하게 됩니다.

 

우선 다음 사진을 살펴봅시다.

위 다이어그램을 보면 Proxy는 한가지 일을 합니다.

우선 database의 connections/transactions 을 열거나 닫고, 실제 UserService에게 나머지 역할을 위임시키게 됩니다.

또한 이를 사용하는 UserRestController 의 입장에서는 이게 Proxy인지? Real인지? 구분할수 없게 됩니다.

 

6. Transaction Manager는 왜 필요할까요?

우리의 코든는 Proxy도 적용 되었고, 더 나아가 그 Proxy가 Transaction을 관리하고 있습니다.

하지만, 트랜잭션의 상태(open, commit, close)를 Proxy 스스로 알아서 결정할수는 없는 노릇 입니다.

따라서 Proxy는 이에 대한 결정을 Transaction Manager에게 위임하게 됩니다.

 

Spring은 기본적으로 몇 가지 편리한 구현체와 함께 제공되는 PlatformTransactionManager / TransactionManager 인터페이스를 제공하는데, 그중 하나가 datasource transaction manager 입니다.

 

transaction manager가 뭘 하는지 살펴보기 전에, 우선 Spring의 Configuration을 잠시 살펴볼까요?

@Bean
public DataSource dataSource() {
    return new MysqlDataSource(); // (1)
}

@Bean
public PlatformTransactionManager txManager() {
    return new DataSourceTransactionManager(dataSource()); // (2)
}

(1) 여기서는 특정 Database에 종속적인 datasource를 생성하고 있습니다.

(2) 여기서는 TransactionManager를 생성하고 있는데, transaction을 관리하기 위해서 data source를 필요로 합니다.

 

모든 transaction manager 들은 "doBegin" 이나 "doCommit" 과 같은 메서드를 가지고 있습니다.

다음 코드는 Spring에서의 코드를 간략화 한 코드 입니다!

public class DataSourceTransactionManager implements PlatformTransactionManager {

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        Connection newCon = obtainDataSource().getConnection();
        // ...
        con.setAutoCommit(false);
        // yes, that's it!
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        // ...
        Connection connection = status.getTransaction().getConnectionHolder().getConnection();
        try {
            con.commit();
        } catch (SQLException ex) {
            throw new TransactionSystemException("Could not commit JDBC transaction", ex);
        }
    }
}

 

datasource transaction manager를 보면 맨 위에서 봤었던 JDBC가 transaction을 관리하는 방식과 정확하게 동일한것을 확인할 수 있습니다. setAutoCommit(false) 보이시죠?

 

여기까지의 내용을 그림으로 살펴보면 다음과 같습니다.

요약을 좀 해보면

1. 만약 Spring이 어떠한 Bean에 붙어있는 @Transactional 을 발견한다면, 해당 bean의 동적 proxy를 만들게 됩니다.

2. proxytransaction manager에 접근할 수 있으며, transactions이나 connections을 열고 닫도록 요청합니다.

3. transaction manager는 JDBC 방식으로 connection을 관리할 뿐 입니다.

 

7. 물리적인 Transaction과 논리적 Transaction의 차이는?

 

  • 물리적 트랜잭션 : 실제 JDBC 트랜잭션
  • 논리적 트랜잭션 : 중첩된 @Tansactional을 갖는 메소드

 

8. @Transactional Propagation Levels 의 용도는?

@Transactional에 옵션으로 줄 수 있는 Propagation level에는 여러가지가 있습니다. 

@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
// 기타 등등...
  • Required(default) : 메소드는 트랜잭션을 필요로 해. 트랜잭션을 새로 하나 열든지, 기존에 있던 거를 쓰든지 할거야 => getConnection(); setAutoCommit(false); commit();
  • Supports : 트랜잭션을 열든말든 상관 안할거야, 그냥 잘 실행할 수 있어. => JDBC는 아무것도 안함
  • Mandatory : 스스로 트랜잭션을 열진 않을거지만, 아무도 트랜잭션을 열지 않으면 예외를 던질꺼! => JDBC는 아무것도 안함
  • Required_new : 온전히 나의 소유인 트랜잭션이 필요해. => getConnection(); setAutoCommit(false); commit();
  • Not_Supported : 트랜잭션 싫어. 이미 실행중인 트랜잭션이 있으면 중지시킬거야 => JDBC는 아무것도 안함
  • Never : 누군가 트랜잭션을 시작시킨다면 예외를 던질거야 => JDBC는 아무것도 안함
  • Nested : 복잡하군... 하지만 걱정마! 저장점을 잡아줄게! => connection.setSavepoint()

보다시피 대부분의 전파 모드는 데이터베이스나 JDBC와는 상관이 없어요!

단지 Spring과 프로그램을 어떻게 구성하는지, 그리고 언제/어디서/어떻게 트랜잭션이 적용될지를 예상할수 있습니다.

 

예를 들어 다음 코드를 살펴볼까요?

public class UserService {

     @Transactional(propagation = Propagation.MANDATORY)
     public void myMethod() {
        // execute some sql
     }

}

 

Spring은 UserService 클래스의 myMethod()를 호출할 때마다 트랜잭션이 열려 있을 것으로 예상합니다.

자기 스스로 트랜잭션을 열지는 않는거죠! 만약 사전에 트랜잭션을 열지 않고 호출하게 된다면 예외를 발생시키게 됩니다.

 

9. @Transactional Isolation Levels 의 용도는?

@Transactional(isolation = Isolation.REPEATABLE_READ)

위 코드와 같이 isolation 옵션을 지정해 주면 Proxy에서 아래와 같이 만들어 줍니다.

connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

데이터베이스 격리 수준은 시간을 어느정도 투자해야 하는 복잡한 주제입니다.

트랜잭션 중에 격리 수준을 전환할 때, 사용하고 있는 데이터베이스나 JDBC 드라이버에서 기능이 지원되는지를 분명하게 먼저 확인해야할 필요가 있습니다. 각 데이터베이스 회사마다 지원하는 기본 격리 수준이 다르기 때문입니다.

 

10. @Transactional 사용시 흔히 범하는 실수

예를들어 다음과 같은 코드가 있다고 해봅시다.

@Service
public class UserService {

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

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

위 코드에서 invoice() 메서드를 호출하면 Transaction이 적용되게 됩니다. 내부의 createPdf()를 호출할때 또한 마찬가지로 적용되죠

그럼 invoice() 메서드를 호출할때 몇개의 물리적인 Transaction이 열리게 될까요?

 

바로 1개 입니다!

 

Spring은 사용자를 위해 트랜잭션 UserService proxy를 생성하지만, 일단 UserService 클래스 안에 들어가서 다른 내부 메서드를 호출하게 되면 더 이상 프록시가 관련되지 않습니다. 다시 말해 새로운 Transaction이 생기지 않습니다!

위 사진에서 보이듯, Proxy 에서 Transaction을 연 시점부터 ~ 닫는 시점까지 그안에 싱행된 real UserSerivce에 메서드들은 동일한 Transaction 범위 안에서 수행됩니다.

 

@Transactional 은 아니지만 스프링 프록시와 관련된 문제를 다룬 포스트에서 참고하시면 좋겠습니다.

 

핵심

Hibernate를 쓰든 JPA를 쓰든 @Transactional 을 쓰든 JDBC 기본(getConnection(), setAutoCommit(false), commit())방식 이 가장 중요합니다.

이 뼈대(JDBC 기본 접근 방식)만 알고 있으면 트랜잭션을 조작할 때, 추가적으로 어떤 일이 일어나는지에 대해서 이해하기 조금 더 쉬워집니다.

 

참고

 

Spring Transaction Management: @Transactional In-Depth

You can use this guide to get a simple and practical understanding of how Spring's transaction management with the @Transactional annotation works.

www.marcobehler.com

 

 

Transaction 동작 원리, JDBC 기본에 충실한 과정(JPA, Hibernate, Spring's @Transactional)

JDBC 트랜잭션에 대한 이해 Transaction 기본 동작 원리를 익히기 위해서 점진적인 접근을 할 것입니다. (두괄식X 미괄식O) 궁극적으로는 @Transactional 이 어떻게 돌아가는지를 알아볼 것이지만, 우선은

jeong-pro.tistory.com

 

'BackEnd > Spring' 카테고리의 다른 글

[Spring] CORS 문제 해결하기  (0) 2022.04.26
[Spring] Thymeleaf 에러(Error resolving template)  (0) 2022.03.19
[Spring] @SpringBootTest  (0) 2022.03.07
[Spring] 빈 스코프  (0) 2022.02.08
[Spring] 빈 생명주기 콜백  (0) 2022.02.07

댓글