본문 바로가기

Back-End/토비의 스프링3

5-2장. 스프링 - 트랜잭션 서비스 추상화

사용자 레벨 업그레이드 코드를 완성했다. 하지만, 레벨 업그레이드 작업 중 에러가 발생하여 일부 유저만 수정되었다면 어떻게 처리해야할까?


고객들의 불만을 일으키지 않기 위해, 원상태로 돌려놓아야 한다.


즉, upgradeLevels()를 하나의 작업단위인 트랜잭션을 적용해야 한다.


JDBC 트랜잭션의 트랜잭션 경계설정

setAutoCommit(false)로 트랜잭션의 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션 경계설정이라 한다.
Connection c = dataSource.getConnection();

c.setAutoCommit(false); // 트랜잭션 시작

try { // 트랜잭션 하나의 작업
PreparedStatement st1 = c.prepareStatement("update users ... ");
st1.executeUpdate();

PreparedStatement st2 = c.prepareStatement("delete users ... ");
st2.executeUpdate();

c.commit();// 트랜잭션 커밋
} catch (Exception e) {
c.rollback();
}

그렇다면, upgradeLevels() 함수 내에 위와 같은 코드를 추가해야 할까? 이 방식은 비즈니스 로직과 데이터 로직을 한데 묶어버리는 한심한 결과를 초래한다.


  1. JDBC API를 직접 이용하기 때문에 JPA나 다른 데이터 엑세스 기술에 독립적일 수가 없다.
  2. 테스트 코드에서도 Connection  오브젝트를 일일이 만들어 줘야한다.
  3. 쓸데없이 UserService의 함수끼리 Connection 파라미터를 전달해줘야 한다.

스프링을 이용한 트랜잭션 동기화 - Connection을 직접 쓰지말자!


upgradeLevels() 함수가 트랜잭션 경계설정을 해야 한다는 사실은 피할 수 없다. 따라서 그 안에서 Connection을 생성하고 트랜잭션 시작과 종료를 관리하게 해야 한다.

대신 Connection 오브젝트를 계속 함수의 파라미터로 전달하는 건 피하고 싶다.

이를 위해 스프링이 제안하는 방법은 독립적인 트랜잭션 동기화 방식이다.


(1) UserService는 Connection을 생성

(2) 트랜잭션 동기화 저장소에 저장해두고 Connection의 setAutoCommit(false)를 호출해 트랜잭션을 시작시킨 후에 DAO 기능 이용 시작

(3) update() 호출, 

(4) JdbcTemplate 메소드에서 트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection 오브젝트가 존재하는지 확인 / (2)에서 생성한 Connection을 가져옴

(5) Connection을 이용하여 수정 SQL을 실행. Connection은 닫지 않은채 DB 작업을 마침

(6~11) 반복

(12) Connection의 commit()을 호출해서 트랜잭션 완료시킴

(13) 트랜잭션 저장소가 더 이상 Connection 오브젝트를 저장해두지 않도록 이를 제거


public void upgradeLevels() throws Exception {
TransactionSynchronizationManager.initSynchronization();
// 트랜잭션 동기화 관리자를 이용해 동기화 작업을 초기화

Connection c = DataSourceUtils.getConnection(dataSource);
// DB 커넥션 생성과 동기화를 함께 해주는 유틸리티 메소드
c.setAutoCommit(false);

try {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user))
upgradeLevel(user);
}
c.commit();// 트랜잭션 커밋
} catch (Exception e) {
c.rollback();// 트랜잭션 롤백
throw e;
} finally {
DataSourceUtils.releaseConnection(c, dataSource); // DB 커넥션을 안전하게 닫는다.

// 동기화 작업 종료 및 정리
TransactionSynchronizationManager.unbindResource(this.dataSource);
TransactionSynchronizationManager.clearSynchronization();
}
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}

5.2.4. 트랜잭션 서비스 추상화 

지금까지 데이터 엑세스 부분과 비즈니스 로직을 잘 분리, 유지할 수 있게 만든 코드를 만들었다. 

하지만, 고객측에서 DB연결 방법을 바꾸고 싶다는 요구가 온다면?
  • DataSource 인터페이스와 DI를 적용한 덕분에 UserDao,UserService는 수정하지 않아도 된다.
  • UserService에서 JDBC의 Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 다른 DB의 트랜잭션을 보장해주지 못한다. 즉, 별도의 트랜 잭션 관리자를 통해 글로벌 트랜잭션이 필요하다.

트랜잭션 서비스 추상화를 위한 JTA ( Java Transaction API )

자바는 JDBC 외에 이런 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 JTA를 제공하고 있다.

아래 그림은 JTA를 통해 글로벌/분산 트랜잭션이 어떻게 관리되는지 보여주는 그림이다. 
  • 트랜잭션은 JDBC나 JMS API가 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저에게 위임
  • 트랜잭션 매니저는 각각의 리소스 매니저와 XA 프로토콜을 통해 연결

jta를 통한 글로벌 분산 트랜잭션 관리;에 대한 이미지 검색결과

public void sample(){

// JNDI를 이용해 서버의 UserTransaction 오브젝트를 가져온다.
InitialContext ctx = new InitialContext();
UserTransaction tx = ctx.lookup(USER_TX_JNDI_NAME);

tx.begin();
// JNDI로 가져온 dataSource를 사용한다.
Connection c = dataSource.getConnection();

try{
// 데이터 엑세스 코드
tx.commit();
}catch (Exception e){
tx.rollback();
throw e;
}finally{
c.close();
}
}
  • 트랜잭션 경계설정을 위한 구조는 JDBC 사용때와 비슷하다.
  • 문제점 =>로컬 트랜잭션 관리로도 충분한 경우에, UserService는 자신의 로직이 바뀌지 않았음에도 기술환경에 따라서 코드가 바뀌는 코드가 돼버린다.

트랜잭션 API의 의존관계 문제와 해결책

UserService는 UserDAO 인터페이스에만 의존하는 구조였다.
  • DAO 클래스의 구현기술이 JDBC, 하이버네이트, 여타 기술로 바뀌더라도 UserService 코드는 영향을 받지 않는다. ( OCP 원칙 코드 )
  • 문제는 JDBC에 종속적인 Connection을 이용한 트랜잭션 코드가 UserService에 등장하는 것이다.
트랜잭션도 데이터엑세스 기술처럼 특정 기술에 의존적이지 않게 수정을 해보자.

스프링의 트랜잭션 서비스 추상화

다행히 트랜잭션의 트랜잭션 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조다. 즉, 추상화가 필요하다.

[ 스프링이 제공하는 트랜잭션 추상화 계층구조 ]

스프링 트랜잭션 추상화 계층에 대한 이미지 검색결과

스프링이 제공하는 트랜잭션 추상화 방법을 UserService에 적용한 코드이다.
public void upgradeLevels() {

// JDBC 트랜잭션 추상 오브젝트 사용
PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);

TransactionStatus status =
transactionManager.getTransaction(new DefaultTransactionDefinition());

try {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user))
upgradeLevel(user);
}
transactionManager.commit(status);// 트랜잭션 커밋

트랜잭션 기술 설정의 분리

위의 코드를 보면 UserService가 어떤 트랜잭션 매니저 구현 클래스를 사용할지를 스스로 알고 있다. 이는 DI 원칙에 위배된다. 자신이 사용할 구체적인 클래스를 스스로 결정하고 생성하지 말고 컨테이너를 통해 외부에서 제공받게 하는 스프링의 DI 방식으로 바꾸자.

public class UserService {
private PlatformTransactionManager transactionManager;

public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}

public void upgradeLevels() {

// DI 받은 트랜잭션 매니저를 공유해서 사용한다. ( 멀티스레드 환경에서도 안전 )
TransactionStatus status =
this.transactionManager.getTransaction(new DefaultTransactionDefinition());


try {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user))
upgradeLevel(user);
}
this.transactionManager.commit(status);// 트랜잭션 커밋
} catch (Exception e) {
this.transactionManager.rollback(status);// 트랜잭션 롤백
throw e;
}
}
<bean id="userService" class="springbook.user.service.UserService">
<property name="userDao" ref="userDao" />
<property name="dataSource" ref="dataSource" />
<property name="transactionManager" ref="transactionManager" />
</bean>

<bean id="transacntionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

이제 UserService는 트랜잭션 기술에서 완전히 독립적인 코드가 됐다.