WEB/Spring

스프링 DB 1편 4) 스프링과 문제 해결 - 트랜잭션

Tony Lim 2022. 6. 16. 14:47
728x90

순수한 서비스 계층

핵심비지니스 로직이 들은 계층임으로 최대한 순수하게 유지해야한다.

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    Connection con = dataSource.getConnection();
    try {
        con.setAutoCommit(false);//트랜잭션 시작
        //비즈니스 로직
        bizLogic(con, fromId, toId, money);
        con.commit(); //성공시 커밋
    } catch (Exception e) {
        con.rollback(); //실패시 롤백
        throw new IllegalStateException(e);
    } finally {
        release(con);
    }

}

private void bizLogic(Connection con, String fromId, String toId, int money) throws SQLException {
    Member fromMember = memberRepository.findById(con, fromId);
    Member toMember = memberRepository.findById(con, toId);

    memberRepository.update(con, fromId, fromMember.getMoney() - money);
    validation(toMember);
    memberRepository.update(con, toId, toMember.getMoney() + money);
}

현재는 비지니스 로직보다 JDBC 트랜잭션 로직이 훨씬 길고 많다. JPA로 바꾸게 되면 다 바꿔야한다.

 

트랜잭션 문제 = JDBC 로직들을 DAO (Repository)에 다 모아 놓았지만 트랜잭션을 적용하면서 결국 서비스 계층에 JDBC 구현 기술의 누수가 발생했다.
또한 try,catch,finally 같은 반복이 많이 일어난다.

예외 누수문제 = SQL Exception 이 체크 예외라 DAO에서 처리하거나 서비스 까지 던져야한다.
또한 JDBC 종속적인 예외 클래스이다.

 


 

다양한 데이터 접근 기술이 데이터 접근 계층 말고도 서비스 계층까지 의존하고 있다. 

jdbc 에서 jpa 로 변경하게 된다면 서비스 계층의 tx 로직까지 다 변경해야한다.

 

트랜잭션 추상화

실제 구현체가 변경이 있어도 서비스 레이어는 TxManager interface만 참조하고 있기에 코드 수정이 필요 없다.

public interface PlatformTransactionManager extends TransactionManager {

   TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
         throws TransactionException;
   void commit(TransactionStatus status) throws TransactionException;
   void rollback(TransactionStatus status) throws TransactionException;
}

스프링에서도 트랜잭션을 추상화를 위해 존재하는 interface가 존재한다.

이미 스프링에서 트랜잭션 인터페이스를 구현한 여러 종류의 데이터 접근 기술을 구현해놓았다.

 

트랜잭션 동기화

트랜잭션을 유지하려면 트랜잭션의 시작부터 끝까지 같은 데이터베이스 커넥션을 유지해야한다. 즉 같은 커넥션을 동기화 하면 된다. (지금까진 parameter로 넘겨주는 방식)

ThreadLocal에 커넥션을 보관하게 된다. 더 이상 parameter로 커넥션을 전달하지 않아도 된다.

동작방식

1. 트랜잭션 시작 요청 -> 트랜잭션 매니저가 Datasource를 통해 (setAutocommit , max.. 등등 설정)  Connection을 얻어온후에 트랜잭션 시작후에 ThreadLocal에 보관하게 된다.

2. 리포지토리에서 실제 db에게 요청을 날릴때 자신이 속한 스레드로컬의 Connection 을 가지고 요청을 날리게 됨

3. 트랜잭션 종료 요청,commit,rollback -> 트랜잭션 매니저가 요청이 들어온 스레드의 스레드로컬의 Connection을 가지고 트랜잭션 종료 요청을 db에게 전달함(사실상 commit, rollback이 이런의미인듯) 이후 Connection.close()

public abstract class TransactionSynchronizationManager {

   private static final ThreadLocal<Map<Object, Object>> resources =
         new NamedThreadLocal<>("Transactional resources");

   private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
         new NamedThreadLocal<>("Transaction synchronizations");

   private static final ThreadLocal<String> currentTransactionName =
         new NamedThreadLocal<>("Current transaction name");

   private static final ThreadLocal<Boolean> currentTransactionReadOnly =
         new NamedThreadLocal<>("Current transaction read-only status");

   private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
         new NamedThreadLocal<>("Current transaction isolation level");

   private static final ThreadLocal<Boolean> actualTransactionActive =
         new NamedThreadLocal<>("Actual transaction active");

resource 스레드 로컬에 보관하는 것을 확인할 수 있다.

 


 

예제 TM적용

private void close(Connection con, Statement stmt, ResultSet rs) {
    JdbcUtils.closeResultSet(rs);
    JdbcUtils.closeStatement(stmt);
    //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
    DataSourceUtils.releaseConnection(con, dataSource);
}


private Connection getConnection() throws SQLException {
    //주의! 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 한다.
    Connection con = DataSourceUtils.getConnection(dataSource);
    log.info("get connection={}, class={}", con, con.getClass());
    return con;
}

TransactionSynchronizationManager를 사용하려면 DataSourceUtils를 통해 Connection 을 사용해야한다. 하지만 트랜잭션 시작 없이 시작하면 

DAO에서 조회시 threadlocal이 비어있을 것임으로 새로운 커넥션을 JDBC로 붙어 얻어와서 동작하게 된다.

 

커넥션도 close를 직접하지 않고 DataSourceUtils #releaseConnection을 사용한다.

트랜잭션 내부에서 사용한 커넥션을 닫지 않고 추후 rollback, commit을 위해 스레드로컬을 위해 남겨둔다. 

서비스 계층에서 넣어준 것이다. (요기서 tx를 시작했으니 , 시작과 동시에 Connection을 threadLocal에 넣어준다.) DAO 계층에서 Connection Close 하는것은 이상한 것이다.

 

PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

TransactionManager에서 getTransaction을 하면 안에서 트랜잭션을 시작하게 된다.

TransactionManager는 생성자를 통해 주입받은 DataSource를 통해서 커넥션을 생성하고 TransactionSynchronizationManager 의 필드인 resource(ThreadLocal) 에 생성한 커넥션을 저장하게 된다.

 


 

예외 트랜잭션 탬플릿 적용

https://tonylim.tistory.com/352

 

스프링 핵심원리 고급편 3) 템플릿 메서드 패턴과 콜백 패턴

템플릿 메서드 패턴 부모클래스에 알고리즘의 골격인 template을 정의하고 일부 변경되는 로직은 자식클래스에서 정의하는것이다. 로그 추적기를 사용하는 구조는 모두 동일하다. 이런 boilerplate

tonylim.tistory.com

public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    txTemplate.executeWithoutResult((status) -> {
        //비즈니스 로직
        try {
            bizLogic(fromId, toId, money);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    });
}

txTemplate에서 transaction begin, commit을 해주는 코드가 존재하고 이 내부에서 실제 호출할 비즈니스 로직을 주입받게 된다.

TransactionTemplate은 TransactionManager를 주입받게 된다.

public <T> T execute(TransactionCallback<T> action) throws TransactionException {
   Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");

   if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
      return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
   }
   else {
      TransactionStatus status = this.transactionManager.getTransaction(this);
      T result;
      try {
         result = action.doInTransaction(status);
      }
      catch (RuntimeException | Error ex) {
         // Transactional code threw application exception -> rollback
         rollbackOnException(status, ex);
         throw ex;
      }
      catch (Throwable ex) {
         // Transactional code threw unexpected exception -> rollback
         rollbackOnException(status, ex);
         throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
      }
      this.transactionManager.commit(status);
      return result;
   }
}

받은 action을 , 즉 비지니스 로직을 실행 시키기만 한다.

하지만 여전히 Txtemplate도 transaction code이다. 이것조차 서비스 레이어에서 분리하고싶다.

 


 

@Transactional AOP

https://tonylim.tistory.com/358

 

스프링 핵심 원리 - 고급편 7) @Aspect AOP , 스프링 AOP 개념

@Aspect AOP AspectJ 에서 제공하는 에노테이션이다. annotation을 차용했을 뿐 실제 내부 구현은 스프링이 한것이다. 진짜 AspectJ를 사용하는것이 아니다. (컴파일 ,로드타임 위버 사용하는것 아님) @Slf4j

tonylim.tistory.com

https://tonylim.tistory.com/357

 

스프링 핵심 원리 - 고급편 6) 빈 후처리기

 일반적인 스프링 빈 등록 과정 public class BasicTest { @Test void basicConfig() { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class); //A는 빈으로 등..

tonylim.tistory.com

@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    bizLogic(fromId, toId, money);
}

Advisor = BeanFactoryTransactionAttributeSourceAdvisor  ==  (Pointcut = TransactionAttributeSourcePointcut)  +  (Advice = TransactionInterceptor)

이것들을 자동으로 빈으로 등록하여 제공해준다. 그러면 빈후처리기에서 @Transactional 이 있는지 reflection을 통해 확인하고 pointcut에 매칭이되면 Proxy bean을 컨테이너에 등록하게 된다.

나중에 실제 요청이 왔을 때 컨테이너에서 Advisor 빈들을 찾아보며 매칭되는 pointcut 에 advice를 적용시켜준다.

 

테스트를 돌릴시에 @SpringBootTest 를 해줘야 스프링을 이용해서 테스트를 진행 가능하다.

스프링부트에서는 application.properties에 적혀있는 정보를 가지고 DataSouce bean을 생성해서 주입해준다.

또한 PlatformTransactionManger 를 구현한 구체클래스를 bean으로 자동생성해서 주입해준다. 이때 뭘 등록할지 현재 등록된 라이브러리를 보고 판단한다.

 

 

 

728x90