서비스 추상화와 디자인 패턴
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
로 받아버린다.
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 |