코드로 스프링 트랜잭션 API 구조 보기

2022. 5. 12. 16:34·Java

서비스 추상화와 디자인 패턴

PSA (Portable Service Abstraction) 는 로우 레벨의 기술을 추상화하여 (Service Abstraction) 다른 애플리케이션에서 적용할 수 있게 해주는 (Portable) 방법이다. 여기서 서비스의 의미를 생각해봐야 하는데, 애플리케이션의 비즈니스 로직 서비스 계층을 말하는 것이 아니라 기술 서비스 계층을 의미한다.

 

 

 

 

위 그림은 토비의 스프링 5장 서비스 추상화에서 등장한 구조도로, 트랜잭션의 서비스 추상화를 위해 계층 간 상속관계와 호출을 나타내고 있다. 트랜잭션 기술의 서비스 추상화를 위해 PlatformTransactionManager 라는 인터페이스를 두고 DataSourceTransactionManager, HibernateTransactionManager 등의 구현체에서 실제 트랜잭션 기능을 구현하고 있다.

 

 

PlatformTransactionManager.java

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(), commit(), rollback() 등 트랜잭션에서 사용되는 기본적인 오퍼레이션만 정의되어 있다. 그리고 DataSourceTransactionManager등은 위 인터페이스의 구현체로 오퍼레이션에 대한 실제 기능을 구현한다. 먼저 TransactionManager 를 구현한 추상 클래스인 AbstractPlatformTransactionManager 에서 각 트랜잭션 구현체에서 공통적으로 사용할 기능을 구현하고, DataSourceTransactionManager 가 상속받는다.

 

 

AbstractPlatfromTransactionManager 를 사용하는 다양한 PlatformTransactionManager 의 구현체 목록은 다음과 같다.

 

 

 

 

AbstractPlatformTransactionManager.java

public abstract class AbstractPlatformTransactionManager {

    @Override
    public final void commit(TransactionStatusstatus) throws TransactionException {
        ...
        processCommit(defStatus);
    }

    private void processCommit(DefaultTransactionStatusstatus) throws TransactionException {
        ...
        doCommit(status);
    }

    protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;

}

 

 

실제 코드는 try-catch 블록과 if-else 블록으로 복잡하게 구성이 되어있지만, 커밋 기능에 대해 상속관계와 오버라이딩을 파악하기 위해 간략하게 나타내면 위와 같다. AbstractPlatformTransactionManager 에서는 PlatformTransactionManager 의 commit() 오퍼레이션을 구현하면서 private 메서드인 processCommit() 을 호출하고, processCommit() 에서는 doCommit() 메서드를 호출한다.

 

하지만 doCommit() 메서드는 접근 제어자가 protected 로, AbstractPlatformTransactionManager 를 상속받은 클래스에서 해당 메서드를 반드시 구현해야한다. 즉, AbstractPlatformTransactionManager 에서 공통적인 기능을 구현하면서, 내부적으로 사용되는 기술 (JDBC, Hibernate 등) 변경이 잦은 기능은 상속받는 클래스에서 doCommit() 을 구현하도록 만든 템플릿 메서드 패턴이다.

 

 

템플릿 메서드 패턴이란?

 

[Design Pattern] 템플릿 메서드 패턴이란 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

 

템플릿 메서드를 구현한 DataSourceTransactionManager 는 다음과 같다.

 

 

DataSourceTransactionManager.java

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager {

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();

        try {
            con.commit();
        }
        catch (SQLException ex) {
            throw translateException("JDBC commit", ex);
        }
    }
}

 

 

DataSourceTransactionManager 에서는 doCommit() 을 오버라이딩해 JDBC 를 이용해 커밋 기능을 구현한다. 하지만 JDBC 의 기능을 로우 레벨로 구현하지 않고, TransactionStatus 객체로부터 Connection 객체를 가져와 commit() 을 실행한다.

 

그리고 템플릿 메서드를 구현한 HibernateTransactionManager 는 다음과 같다.

 

 

HibernateTransactionManager.java

public class HibernateTransactionManager extends AbstractPlatformTransactionManager {

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
        Transaction hibTx = txObject.getSessionHolder().getTransaction();
        Assert.state(hibTx != null, "No Hibernate transaction");
        if (status.isDebug()) {
            logger.debug("Committing Hibernate transaction on Session [" +
            		txObject.getSessionHolder().getSession() + "]");
        }

        try {
            hibTx.commit();
        }
        catch (org.hibernate.TransactionException ex) {
            throw new TransactionSystemException("Could not commit Hibernate transaction", ex);
        }
        catch (HibernateException ex) {
            throw convertHibernateAccessException(ex);
        }
        catch (PersistenceException ex) {
            if (ex.getCause() instanceof HibernateException) {
                    throw convertHibernateAccessException((HibernateException) ex.getCause());
            }
            throw ex;
        }
    }
}

 

 

DataSourceTransactionManager 와 마찬가지로 doCommit() 을 오버라이딩해 Hibernate 의 세션을 이용해 커밋 기능을 구현한다. 하지만 세션의 기능을 로우 레벨로 구현하지 않고, TransactionStatus 객체로부터 getTransaction() 으로 트랜잭션 객체를 가져와 형변환 후 commit() 을 실행한다.

 

즉, DataSourceTransactionManager 와 HibernateTransactionManager 는 PlatformTransactionManager 인터페이스의 구현체로, 클라이언트 객체로서 다른 객체의 메서드를 호출하는 어댑터 패턴으로 구성되어 있다. 클라이언트 객체 (여기서는 비즈니스 로직 서비스 계층) 들이 트랜잭션 기능을 사용할 때에는 공통의 인터페이스를 둬서, 호출하는 클라이언트 객체 모두가 일관적으로 트랜잭션을 처리할 수 있도록 구성하고, 인터페이스의 뒤에서는 어댑터를 이용해 각 구현체마다 다른 기능을 구현한다.

 

 

어댑터 패턴이란?

 

Adapter pattern - Wikipedia

In software engineering, the adapter pattern is a software design pattern (also known as wrapper, an alternative naming shared with the decorator pattern) that allows the interface of an existing class to be used as another interface.[1] It is often used t

en.wikipedia.org

 

 

 

 

이 그림을 다시보면, UserService 객체에서 PlatformTransactionManager 을 호출하고, DataSourceTransactionManager 와 HibernateTransactionManger 에서 각 트랜잭션 기능이 구현된 JDBC, JTA, Hibernate 트랜잭션 서비스의 객체를 호출하는 것으로 이해할 수 있다. 따라서, 서비스 추상화 기능 전체가 인터페이스와 어댑터로만 구성되었다고 봐도 무방하지 않을까?

 

 


 

 

서비스 추상화의 클라이언트

그렇다면 인터페이스인 PlatformTransactionManager 의 commit() 이 호출되었을 때, 어떻게 트랜잭션 경계의 시작에서 생성된 트랜잭션 객체를 다시 커밋 시에 받아서 사용할까?

 

 

 

 

위는 토비의 스프링에서 TransactionManager 를 의존관계 주입받아 서비스 계층 내 메서드에서 트랜잭션 경계를 설정하고, 트랜잭션 경계 내부에 비즈니스 로직을 작성한 모습이다. 여기서 TransactionManager 로부터 getTransaction() 메서드로 TransactionStatus 객체를 반환받고, 트랜잭션을 커밋하거나 롤백할 때 commit() 에 TransactionStatus 를 다시 인자로 입력한다.

 

 

AbstractPlatformTransactionManager.java

public abstract class AbstractPlatformTransactionManager {

    @Override
    public final void commit(TransactionStatusstatus) throws TransactionException {
        ...
        processCommit(defStatus);
    }

    private void processCommit(DefaultTransactionStatusstatus) throws TransactionException {
        ...
        doCommit(status);
    }

    protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;

}

 

 

인자로 입력된 TransactionStatus 객체는 PlatformTransactionManager 를 구현한AbstractPlatformTransactionManager 이 오버라이딩한 commit() 에서 DefaultTransactionStatus 로 형변환되고, doCommit() 을 호출할 때 인자로 입력한다.

 

 

DataSourceTransactionManager.java

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager {

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();

        try {
            con.commit();
        }
        catch (SQLException ex) {
            throw translateException("JDBC commit", ex);
        }
    }
}

 

 

그리고 AbstractPlatformTransactionManager 를 상속받은 DataSourceTransactionManager. HibernateTransactionManager 이 오버라이딩한 doCommit() 에서는 status.getTransaction() 으로 트랜잭션 객체를 호출한 뒤, 각 구현체에서 필요한 타입으로 형변환한다.여기서 어떤 PlatformTransactionManager 의 구현체를 사용하냐에 따라 에서 트랜잭션 객체가 DataSourceTransactionObject 로 변환될지, HibernateTransactionObject로 변환될지가 달라진다.

 

그리고 DataSourceTransactionManager 를 살펴보면, 입력된 status 로부터 getTransaction() 으로 트랜잭션 객체를 받아 Connection 객체를 꺼내고 트랙잭션을 처리한다. 그런데, 위에서 HibernateTransactionManager 의 경우에는 Session 객체를 꺼낸다. JDBC 와 Hibernate ORM 이라는 전혀 다른 기능에서 사용하는 객체를 status 에서 각각 반환하도록 구현한 것일까? 그럼 status 에는 새로운 기능을 추가할 때마다 해당 기능에서 사용할 객체 꺼낼 수 있도록 메서드를 추가해야할까?

 

재밌는 점은 DefaultTransactionStatus 에서 getTransaction() 메서드에서 반환되는 Transaction 객체는 반환형이 Object 이고, 내부의 transaction 속성 또한 타입이 Object 라는 것이다. 하나의 타입을 받으면서 여러 기능에 대한 메서드를 각각 작성하거나, 여러 기능에 대한 타입을 각각 받지 않고 Object 로 받아버린다.

 

 

DefaultTransactionStatus.java

public class DefaultTransactionStatus extends AbstractTransactionStatus {

    private final Object transaction;

    public Object getTransaction() {
        return this.transaction;
    }
}

 

 

하지만 Object 로 받는다고해서, 런타임 에러가 발생할 일은 없다. DataSourceTransactionManager 는 AbstractPlatformTransactionManager 의 템플릿 메서드 doGetTransaction() 을 오버라이딩할 때, 내부에 DataSourceTransactionObject 타입을 반환한다. 따라서 getTransaction() 으로 반환받는 TransactionStatus 객체의 transaction 속성은 타입만 Object 일 뿐 DataSourceTransactionObject 인스턴스이다. 그리고 나중에 다시 DataSourceTransactionManager 의 commit() 메서드가 입력되고나서, 타입을 원래대로 형변환하고 그 메서드를 호출하며 JDBC 의 기능이 실행되기 때문에, 런타임 에러는 발생하지 않는다.

 

 

AbstractPlatformTransactionManager.java

public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager {

    @Override
    public final TransactionStatus getTransaction(@Nullable TransactionDefinitiondefinition) throws TransactionException {

        Object transaction = doGetTransaction();
        ...
        if (isExistingTransaction(transaction)) {
            return handleExistingTransaction(def, transaction, debugEnabled);
        }

        if (def.getPropagationBehavior() == ...) {
            return startTransaction(def, transaction, debugEnabled, suspendedResources);
        }
        else {
            return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
        }
    }
}

DataSourceTransactionManager.java

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager {

    @Override
    protected Object doGetTransaction() {
        DataSourceTransactionObject txObject = new DataSourceTransactionObject();
        txObject.setSavepointAllowed(isNestedTransactionAllowed());
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
        txObject.setConnectionHolder(conHolder, false);
        return txObject;
    }
}

 

 

이것이 가능한 이유는 스프링에서 의존관계 주입으로 UserService 객체를 만들기 때문이다. 의존관계가 주입될 때 PlatformTransactionManager 의 구현체를 주입하고, 각 구현체에서 getTransaction() 을 호출할 때 트랜잭션 객체가 Object 타입으로 포함된 TransactionStatus 객체를 반환한다. 따라서, TransactionStatus 는 여러 구현체에서 반환되어야하는 객체고, 여러 구현체마다 저마다의 트랜잭션 객체가 있으므로 DefaultTransactionStatus 에서는 모든 트랜잭션 객체를 받을 수 있도록 Object 타입으로 트랜잭션 객체를 초기화한다.

 

(지극히 개인적인 견해이지만) DefaultTransactionStatus 는 트랜잭션 경계 시작 시에 트랜잭션 객체를 잠시 저장해두었다가 커밋 시에 꺼내기 위해 존재하는데, 여기서 모든 타입을 받을 수 있도록 구성하지 않았을까하는 생각이 든다.

 

 

정리

트랜잭션 서비스 추상화 구조와 사용된 디자인 패턴과 함께, 실제 트랜잭션 구현 시에 트랜잭션 경계 시작 시에 생성한 트랜잭션을 커밋 시에 다시 어떻게 사용하는지 알 수 있었다.

'Java' 카테고리의 다른 글

Spring Data JDBC 에서 연관관계 매핑  (4) 2022.04.25
열거형을 이용해 if-else 블록 제거하기  (0) 2022.02.19
주소 변환 (address translation) 의 원리  (0) 2022.01.20
JVM 과 스택 프레임 (Stack Frame)  (0) 2022.01.19
스트림 (Stream) API  (0) 2022.01.13
'Java' 카테고리의 다른 글
  • Spring Data JDBC 에서 연관관계 매핑
  • 열거형을 이용해 if-else 블록 제거하기
  • 주소 변환 (address translation) 의 원리
  • JVM 과 스택 프레임 (Stack Frame)
밀러 (miller)
밀러 (miller)
  • 밀러 (miller)
    밀러의 데브로그
    밀러 (miller)
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Java
      • Node.js
      • Production
      • Books
      • Computer Science
      • TIL
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
    • LinkedIn
  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
밀러 (miller)
코드로 스프링 트랜잭션 API 구조 보기
상단으로

티스토리툴바