Spring DB : TranscatinTemplate을 이용한 코드 리팩토링

    이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다.


    TransactionTemplate을 통한 코드 개선

     

       public void accountTransfer(String fromId, String toId, int money) throws SQLException {
            TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
            
            try {
                bizLogic(fromId, toId, money);
                transactionManager.commit(transaction);
            } catch (Exception e) {
                transactionManager.rollback(transaction); // 실패 시, 롤백
                throw new IllegalStateException(e);
                }
        }
    • Transaction을 사용하는 경우를 살펴보면 계속 반복되는 패턴이 있는 것을 확인할 수 있다.
      • Try ~ Catch문을 처리한다.
      • transactionManager.getTranscation()을 한다
      • transcationManager.commit()을 한다.
      • transactionManager.rollBack()을 한다.

    위와 같이 반복되는 패턴의 문제점은 트랜잭션을 처리할 메서드 / 서비스 계층이 많을수록 동일한 코드가 늘어난다는 것이다. 반복되는 부분을 메서드로 일괄적으로 뽑아내면 좋을 것 같다. 그렇지만 위 코드에서 메서드로 뽑아내는 것은 쉽지 않다. 반복되는 부분 사이에 비즈니스 로직이 존재하기 때문이다.

    이런 반복 문제를 해결할 때, 주로 템플릿 콜백 패턴(Strategy 패턴)이 사용된다. 스프링은 개발자들이 편리하게 이 부분을 처리할 수 있도록 Transaction을 위한 템플릿 콜백 패턴을 구현해두었고, 개발자는 이것을 가져와서 쓰기만 하면 된다. 이 때, 스프링이 제공해주는 것이 TranscationTemplate이다.

     

    TranscationTemplate 구성

    public class TransactionTemplate extends DefaultTransactionDefinition
          implements TransactionOperations, InitializingBean {
    
       private PlatformTransactionManager transactionManager;
    
       public <T> T execute(TransactionCallback<T> action) throws TransactionException {}
       
       void executeWithoutResult(Consumer<TransactionStatus> action) throws TransactionException {}
       }

    스프링은 Transcation의 반복 코드 문제 해결을 위해 Transcation 처리를 위한 Template Callback 패턴을 제공한다. TranscationTemplate을 사용하면 된다.

     

    • execute() : 응답값이 있을 때 사용한다.
    • executeWithouResult() : 응답값이 없을 때 사용한다. 

    가장 핵심 코드는 위 두 가지다. 위 두 가지를 이용해서 복잡했던 코드를 단순하게 만들 수 있다. 위 코드들은 Try ~ Catch 문 + Commit / RollBack + 자원 회수를 적절히 처리해준다. 개발자는 비즈니스 로직에만 집중하면 된다.

     

    트랜잭션 템플릿 사용 로직

    @RequiredArgsConstructor
    @Slf4j
    public class MemberServiceV3_2 {
    
        private final TransactionTemplate transactionTemplate;
        private final MemberRepositoryV3 memberRepository;
    
        // 계좌이체 로직
        public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    
            // 트랜잭션 시작
            transactionTemplate.executeWithoutResult(status -> {
                try {
                    bizLogic(fromId, toId, money);
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            });
        }
    
    
    ....   
    
    
    }
    • TranscationTemplate을 사용하게 되면 TransactionTemplate을 DI해서 사용하면 된다.
    • TransacionTemplate은 트랜잭션 시작 / Commit / RollBack / Try ~ Catch / Connection의 Close를 해결해준다.
    • TranscationTemplate의 execute / executeWithoutResult로 비즈니스 로직을 트랜잭션 내에서 처리한다.
      • execute를 할 때, 람다 함수로 시작하는데 이 때 TransactionStatus를 넘겨준다.
      • 앞서 TranscationManager를 이용할 때, Commit / Rollback에 TranscationStatus가 필요했던 경우를 생각하면 된다.

    트랜잭션 템플릿의 기본 동작은 다음과 같다.

    • 정상 수행 시 트랜잭션을 커밋한다.
      • 언체크 예외가 발생하면 롤백한다. 그 외의 경우 커밋한다. 체크 예외는 커밋한다.
      • 코드에서 예외를 처리하기 위해 Try ~ Catch가 들어갔다. bizLogic() 메서드를 호출하면,  SQLException 체크 예외를 던진다. 해당 람다에서는 체크 예외를 밖으로 던질 수 없기 때문에 언체크 예외로 바꾸어 던지도록 예외를 전환했다. 

     

    코드 작성

    MemberServiceV3_2

    @RequiredArgsConstructor
    @Slf4j
    public class MemberServiceV3_2 {
    
        private final TransactionTemplate transactionTemplate;
        private final MemberRepositoryV3 memberRepository;
    
        // 계좌이체 로직
        public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    
            // 트랜잭션 시작
            transactionTemplate.executeWithoutResult(status -> {
                try {
                    bizLogic(fromId, toId, money);
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            });
        }
        ...
     }
    • MemberServiceV3_2 코드를 다음과 같이 작성한다.
    • TranscationManager를 사용했을 때는 Transcation에서 Connection을 얻어오고, Commit / RollBack / Resource Release를 모두 개발자가 반복적으로 구현해주어야 했다.
    • 현재 코드에서는 TranscationTemplate을 주입받아 사용하고, TranscationTemplate은 내부적으로 TranscationManager를 사용한다. TransactionTemplate은 Connection과 관련된 일체의 작업을 처리해준다.

     

    MemberServiceV3Test_2

    @Slf4j
    @SpringBootTest
    class MemberServiceV3Test_2 {
    
        private MemberRepositoryV3 memberRepository;
        private MemberServiceV3_2 memberService;
    
        @BeforeEach
        void beforeEach() {
    
            DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
            PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
            TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
            memberRepository = new MemberRepositoryV3(dataSource);
            memberService = new MemberServiceV3_2(transactionTemplate, memberRepository);
        }
        ...
        
     }
    • 테스트 코드는 TranscationManager → TransactionTemplate을 주입하도록 beforeEach 메서드만 변경한 것을 빼면 동일하다.

     

     

    TransacionTemplate 정리

    • TransacionTemplate는 트랜잭션을 사용할 때 반복하는 Try ~ Catch / Commit / Rollback 등을 일괄적으로 처리해준다.
    • TransationTemplate은 한 가지 문제점을 남긴다.
      • Service 계층이 Transaction에 의존한다는 것이다. → txTemplate.execute()
      • 서비스 계층에서 트랜잭션을 쓰지 않는 것으로 변경된다면, 위 코드를 모드 수정해줘야한다.
    • 어플리케이션을 구성하는 로직을 핵심 / 부가 기능으로 구분하면, Transcation은 부가 기능이다.
      • 핵심 + 부가 기능이 함께 존재하면 코드의 복잡도가 올라가 유지 보수가 어렵다.

    정리하면 TranscationTemplate은 코드 반복을 줄여주는 좋은 기술이지만, 여전히 서비스 계층이 트랜잭션 기술에 종속적이기 때문에 코드 유지보수 관점에서 어려움이 있다는 것이다. 어떻게 이 문제를 해결할 수 있을까? 

    댓글

    Designed by JB FACTORY