ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 DB - 트랜잭션 적용1
    Data Base/스프링 DB 2023. 11. 24. 17:23

    트랜잭션 적용1

    실제 애플리케이션에서 DB 트랜잭션을 사용해서 계좌이체 같이 원자성이 중요한 비지니스 로직을 어떻게 구현하는지 알아보자 먼저 트랜잭션 없이 단순하게 계좌이체 비지니스 로직만 구현해보자.

     

    MemberServiceV1

    package hello.jdbc.service;
    
    
    import hello.jdbc.domain.Member;
    import hello.jdbc.repository.MemberRepositoryV1;
    import lombok.RequiredArgsConstructor;
    
    import java.sql.SQLException;
    
    
    @RequiredArgsConstructor
    public class MemberServiceV1 {
    
        private  final MemberRepositoryV1 memberRepository;
    
        public void accountTransfer(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("이체중 예외 발생");
            }
        }
    }
    • formId의 회원을 조회해서 toId의 회원에게 money만큼의 돈을 계좌이체 하는 로직이다.
      • fromId 회원의 돈을 money 만큼 감소시킨다. → UPDATE SQL 실행
      • toId 회원의 돈을 money 만큼 증가한다. → UPDATE SQL 실행
    • 예외 상황을 테스트해보기 위해 toId가  "ex"인 경우 예외를 발생한다.

     

    MemberServiceV1Test

    package hello.jdbc.service;
    
    import com.zaxxer.hikari.HikariDataSource;
    import hello.jdbc.connection.ConnectionConst;
    import hello.jdbc.domain.Member;
    import hello.jdbc.repository.MemberRepositoryV1;
    import org.junit.jupiter.api.*;
    
    
    import java.sql.SQLException;
    
    import static hello.jdbc.connection.ConnectionConst.*;
    import static org.junit.jupiter.api.Assertions.*;
    
    /**
     * 기본 동작, 트랜잭션이 없어서 문제가 발생
     */
    class MemberServiceV1Test {
        public static final String MEMBER_A ="member_A";
        public static final String MEMBER_B ="member_B";
        public static final String MEMBER_EX ="ex";
    
        private MemberRepositoryV1 memberRepository;
        private MemberServiceV1 memberService;
        
        @BeforeEach
        void before(){
            HikariDataSource dataSource = new HikariDataSource();
            dataSource.setJdbcUrl(URL);
            dataSource.setUsername(USERNAME);
            dataSource.setPassword(PASSWORD);
    
            memberRepository = new MemberRepositoryV1(dataSource);
            memberService = new MemberServiceV1(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(),8000);
            Assertions.assertEquals(findMemberB.getMoney(),10000);
    
    
        }
    
    }

     

     

    정상이체 - accountTransfer();

    • given : 다음 데이처를 저장해서 테스트를 준비한다.
      • memberA 10000원
      • memberB 10000원
    • when : 계좌이체 로직을 실행한다.
      • memberService.accountTransfer( )를 실행한다.
      • memberA → memberB로 2000원 계좌에이체 한다.
        • memberA의 금액이 2000원 감소한다.
        • memberB의 금액이 2000원 증가한다. 
    • then : 계좌이체가 정상 수행되었는지 검증한다.
      • memberA 8000원 - 2000원 감소
      • memberB 12000원 - 2000원 감소

    정상 이체 로직이 정상 수행 되는 것을 확인할 수 있다.

     

    테스트 데이터 제거

    테스트가 끝나면 다음 테스트에 영향을 주지 않기 위해 @AfterEach에서 테스트에 사용한 데이터를 모두 삭제한다.

    • @BeforeEach : 각각의 테스트가 수행되기 전에 실행된다.
    • @AfterEach : 각각의 테스트가 실행되고 난 이후에 실행된다.
        @AfterEach
        void after() throws SQLException {
            memberRepository.delete(MEMBER_A);
            memberRepository.delete(MEMBER_B);
            memberRepository.delete(MEMBER_EX);
        }
    • 테스트 데이터를 제거하는 과정이 불편하지만, 다음 테스트에 영향을 주지 않으려면 테스트에서 사요한 데이터를 모두 제거해야 한다. 그렇지 않으면 이번 테스트에서 사용한 데이터 때문에 다음 테스트에서 데이터 중복으로 오류가 발생할 수 있다.
    • 테스트에서 사용한 데이터를 게거하는 더 나은 방법으로는 트랜잭션을 활용하면 된다. 테스트 전에 트랜잭션을 시작하고, 테스트 이후에 트랜잭션을 롤백해버리면 데이터가 처음 상태로 돌아돈다. 이 방법은 이후에 설명한다.

     

    이체중 예외 발생 - accountTransferEx()

    • given : 다음 데이터를 지정해서 테스트를 준비한다.
      • memberA 1000원
      • memberEx 1000원
    • when : 계좌이체 로직을 실행한다.
      • memberService.accountTransfer()를 실행한다.
      • memberA → memberEx로 2000d원 계좌이체 한다.
        • memberA의 금액이 2000원 감소한다.
        • memberEx회원의 ID는 ex이므로 중간에예외가 발생한다. → 이부분이 중요하다.
    • then : 계좌이체는 실패한다. memberA의 돈만 2000원 줄어든다.
      • memberA 8000원 → 2000원 감소
      • memberB 10000원 - 중간에 실패로 로직이 수행되지 않았다 따라서 그대로 10000원으로 남아있게 된다.

     

    정리

    이체중 예외가 발생하게 되면 memberA의 금액은 10000원 → 8000원으로 2000원 감소한다. 그런데 memberB의 돈은 그대로 10000원 인 남아있다. 결과적으로 memberA의 돈만 2000원 감소한 것이다.

     

    [출저 - 스프링 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.