ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 DB - 트랜잭션 문제 해결(트랜잭션 템플릿)
    Data Base/스프링 DB 2023. 11. 28. 13:50

    트랜잭션 문제 해결(트랜잭션 템플릿)

    트랜잭션을 사용하는 로직을 살펴보녕 다음과 같은 패턴이 반복되는 것을 확인할 수 있다.

     

    트랜잭션 사용 코드

    TransactionStatus status = 
    transactionMnager.getTransaction(new DefaultTransactionDefinition());
    
    try{
        //비지니스 로직
        bizLogic(fromId,toId,money);
        //성공시 커밋
        transactionManager.commit(status);
    }catch(Exception e){
    	//실패시 롤백
    	transactionManager.rollback(status);
        throw new IlleagalStateException(e);
    }
    • 트랜잭션을 시작하고, 비지니스 로직을 실행하고, 성고하면 커밋하고, 예외가 발생해서 실패하면 롤백한다.
    • 다른 서비스에서 트랜잭션을 시작하려면 `try`,`catch`,`finally`를 포함한 성공시 커밋, 실패시 롤백 코드가 반복될 것이다.
    • 이런 형태는 각각의 서비스에서 반복된다. 달라지는 부분은 비지니스 로직 뿐이다.
    • 이럴 때 템플릿 콜백 패턴을 활용하면 이런 반복 문제를 깔끔하게 해결할 수 있다.

     

    트랜잭션 템플릿

    템플릿 콜백 패턴을 적용하려면 템플릿을 제공하는 클래스를 작성해야 하는데, 스프링은 `TransactionTemplate`라는 템플릿 클래스를 제공한다.

     

    TransactionTemplate

    public class TransactionTemplate{
    	
        private PlatformTransactionMnager transactionManager;
        
        public <T> T execute(TransactionCallback<T> action){...}
        void executeWithourResult(Consumer<TransactionStatus>action){...}
    }
    • execute(): 응답 값이 있을때 사용한다.
    • executeWithoutResult(): 응답 값이 없을 때 사용한다.

     

    MemberServiceV3_2

    package hello.jdbc.service;
    
    
    import hello.jdbc.domain.Member;
    import hello.jdbc.repository.MemberRepositoryV3;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.transaction.PlatformTransactionManager;
    import org.springframework.transaction.TransactionStatus;
    import org.springframework.transaction.support.DefaultTransactionDefinition;
    import org.springframework.transaction.support.TransactionTemplate;
    
    import java.sql.SQLException;
    
    /**
     * 트랜잭션 - 트랜잭션 템플릿
     */
    @Slf4j
    public class MemberServiceV3_2 {
    
    //    private  final PlatformTransactionManager transactionManager;
        private final TransactionTemplate txTemplate;
        private  final MemberRepositoryV3 memberRepository;
    
        public MemberServiceV3_2(PlatformTransactionManager transactionManager,
        						 MemberRepositoryV3 memberRepository) {
            this.txTemplate = new TransactionTemplate(transactionManager);
            this.memberRepository = memberRepository;
        }
    
        public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    
            txTemplate.executeWithoutResult((status)->{
                //비지니스 로직
                try {
                    bizLogic( fromId, toId, money);
                } catch (SQLException e) {
                    //체트 error를 런타임 error로 바꿔서 더진다.
                    throw new RuntimeException(e);
                }
    
            });
    
        }
    
        private void bizLogic(String fromId, String toId, int money) throws SQLException {
            Member fromMember = memberRepository.findById(fromId);
            Member toMember = memberRepository.findById(toId);
    
            memberRepository.update(fromId,fromMember.getMoney() - money);
            validation(toMember);
            memberRepository.update(toId,toMember.getMoney() + money);
        }
    
    
        private void validation(Member toMember) {
            if(toMember.getMemberId().equals("ex")){
                throw new IllegalStateException("이체중 예외 발생");
            }
        }
    }
    • TransactionTemplate을 사용하려면 `transactionManager`가 필요하다. 생성자에서 `transactionManager`를 주입 받으면서 `TransactionTemplate`을 생성했다.

     

    트랜잭션 템플릿 (템플릿 콜백 패턴) 사용 로직

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
    
        txTemplate.executeWithoutResult((status)->{
            //비지니스 로직
            try {
                bizLogic( fromId, toId, money);
            } catch (SQLException e) {
                //체트 error를 런타임 error로 바꿔서 더진다.
                throw new RuntimeException(e);
            }
    
        });
    
    }
    • 트랜잭션 템플릿 덕부에 트랜잭션을 시작하고, 커밋하거나 롤백하는 코드가 모두 제거되었다.
    • 트랜잭션 템플릿의 기본 동작은 다음과 같다.
      • 비지니스 로직이 정상 수행되면 커밋한다.
      • 언체크 예외가 발생하면 롤백한다. 그 외의 경우 커밋한다.(언체크 예외나 런타임 오류만 롤백하고,체크 예외의 경우에는 커밋한다.)
    • 코드에서 예외 처리를 하기 위해 `try~catch`가 들어갔는데, `bizLogic()`메서드를 호출하면 `SQLException`체크 예외를 넘겨준다. 위 코드의 람다에서 체크 예외를 밖으로 던질 수 없기 때문에 언체크(런타) 예외로 바꾸어 던지도록 예외를 전환했다.

     

    MemberServiceV3_2

    package hello.jdbc.repository;
    
    import com.zaxxer.hikari.HikariDataSource;
    import hello.jdbc.domain.Member;
    import hello.jdbc.service.MemberServiceV3_1;
    import hello.jdbc.service.MemberServiceV3_2;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.*;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.transaction.PlatformTransactionManager;
    
    import java.sql.SQLException;
    
    import static hello.jdbc.connection.ConnectionConst.*;
    
    
    /**
     * 트랜잭션 - 트랜잭션 템플릿
     */
    @Slf4j
    class MemberRepositoryV3_2Test {
        public static final String MEMBER_A ="member_A";
        public static final String MEMBER_B ="member_B";
        public static final String MEMBER_EX ="ex";
    
        private MemberRepositoryV3 memberRepository;
        private MemberServiceV3_2 memberService;
    
        @BeforeEach
        void before(){
            HikariDataSource dataSource = new HikariDataSource();
            dataSource.setJdbcUrl(URL);
            dataSource.setUsername(USERNAME);
            dataSource.setPassword(PASSWORD);
            memberRepository = new MemberRepositoryV3(dataSource);
    
            PlatformTransactionManager transactionManager= 
            	new DataSourceTransactionManager(dataSource);
            memberService = new MemberServiceV3_2(transactionManager,memberRepository);
        }
    
        @AfterEach
        void after() throws SQLException {
            memberRepository.delete(MEMBER_A);
            memberRepository.delete(MEMBER_B);
            memberRepository.delete(MEMBER_EX);
        }
    
        @Test
        @DisplayName("정상 이체")
        void accountTransfer() throws SQLException {
            //given
            Member memberA = new Member(MEMBER_A, 10000);
            Member memberB = new Member(MEMBER_B, 10000);
            memberRepository.save(memberA);
            memberRepository.save(memberB);
    
            //when
            memberService.accountTransfer(memberA.getMemberId(),memberB.getMemberId(),2000);
    
            //then
            Member findMemberA = memberRepository.findById(memberA.getMemberId());
            Member findMemberB = memberRepository.findById(memberB.getMemberId());
    
            Assertions.assertEquals(findMemberA.getMoney(),8000);
            Assertions.assertEquals(findMemberB.getMoney(),12000);
    
    
        }
    
    
        @Test
        @DisplayName("이체중 예외 발생")
        void accountTransferEx() throws SQLException {
            //given
            Member memberA = new Member(MEMBER_A, 10000);
            Member memberEx = new Member(MEMBER_EX, 10000);
            memberRepository.save(memberA);
            memberRepository.save(memberEx);
    
            //when
            Assertions.assertThrows(IllegalStateException.class,
                    () -> memberService.accountTransfer(memberA.getMemberId(),
                            memberEx.getMemberId(),2000));
    
    
            //then
            Member findMemberA = memberRepository.findById(memberA.getMemberId());
            Member findMemberB = memberRepository.findById(memberEx.getMemberId());
    
            Assertions.assertEquals(findMemberA.getMoney(),10000);
            Assertions.assertEquals(findMemberB.getMoney(),10000);
    
    
        }
    
    }
    • 테스트 내용은 기존과 같다.
    • 테스트를 실행해보면 정상 동작하는 로직과, 실패시 로직도 잘 수행되는 것을 확인 할 수 있다.

     

    정리

    • 트랜잭션 템플릿 덕분에, 트랜잭션을 사용할 때 반복하는 코드를 제거 할 수 있다.
    • 하지만 서비스 레이어에 비지니스 로직 뿐만 아니라 트랜잭션을 처리하는 기술 로직이 함께 포함되어 있다.
    • 애플리케이션을 구성하는 로직을 핵심 기능과 부가 기능으로 구분하자면 서비스 입장에서 비지니스 로직은 핵심 기능이고, 트랜잭션은 부가 기능이다.
    • 이렇게 비지니스 로직과 트랜잭션을 처리하는 기술 로직이 한곳에 있으면 두 관심사를 하나의 클래스에서 처리하게 된다. 결과적으로 코드를 유지보수하기 어려워진다.
    • 서비스 로직은 가급적 핵심 비지니스 로직만 있어야 한다. 하지만 트랜잭션 기술을 사용하려면 어쩔 수 없이 트랜잭션 코드도 존재해야 한다. 어떻게 하면 이 문제를 해결할 수 있을까?

     

     

     

     

    [출저 - 스프링 DB 1편 - 데이터 접근 핵심 원리, 김영한]

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1

     

    스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의

    백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔

    www.inflearn.com

     

     

    댓글

Designed by Tistory.