스프링/5. 스프링 DB-1

21. 이체 트랜잭션 적용

sdafdq 2023. 9. 30. 02:26

우선, 커밋모드에 대한 적용은 세션마다 이므로, 

커넥션을 얻는 것은 한 섹션을 생성한 다음 그 섹션과의 소통을 위한 객체를 얻는 것이니

 

우리가 커넥션을 지정해서 줄 필요가 있다.

 

 

@Slf4j
public class MemberRepositoryV2 {
    private final DataSource dataSource;

    public Member findById(Connection con, String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);

            rs = pstmt.executeQuery();
            if(rs.next()){
                Member member =new Member();
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.getInt("money"));
                return member;
            }else{
                throw new NoSuchElementException("member not found memberId = " + memberId);
            }
        } catch (SQLException e){
            throw e;
        }finally {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
        }
    }

    public void update(Connection con, String memberId, int money){
        String sql = "update member set money=? where member_id=?";

        PreparedStatement pstmt = null;

        try{
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int result = pstmt.executeUpdate();
            log.info("result = {}",result);
        }catch(SQLException e){
            log.info("error",e);
        }finally {
            JdbcUtils.closeStatement(pstmt);
        }
    }

    public Connection getConnection() throws SQLException {
        Connection con = dataSource.getConnection();
        return con;
    }
}

추가시킨 메소드만 넣어봤다. 

findById(), update()는 커넥션을 받을 수 있는걸로 오버로딩 했다. 커넥션을 인자로 받으면 그 커넥션을 사용토록 바꿨다.

커넥션을 닫는 부분도, 트랜잭션 동안 커넥션이 닫히면(풀에 반환되면) 안되기에 따로 처리를 했다. (finaly에 직접적으로)

 

getConnection은 public으로 바꿔 외부에서도 이용할 수 있게끔 했다.

 

원래 Service에서 DataSource를 주입받아 사용토록 하던데,

나는 DB와의 소통은 리포지토리에서 되어야지, 라고 생각하여서 리포지토리를 통하여 받을 수 있도록 하였다.

 

 

@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {
    private final MemberRepositoryV2 memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Connection con = memberRepository.getConnection();
        try{
            con.setAutoCommit(false);
            transferLogic(con, fromId, toId, money);
            con.commit();
        }catch(Exception e){
            con.rollback();
            throw new IllegalStateException();
        }finally {
            release(con);
        }
    }

    private void transferLogic(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);
    }

    private static void release(Connection con) {
        if(con != null){
            try{
                con.setAutoCommit(true);
                con.close();
            }catch (Exception e){
                log.info("close error = ",e);
            }
        }
    }

    private void validation(Member toMember) {
        if(toMember.getMemberId().equals("ex")){
            throw new IllegalStateException("이체 중 예외 발생");
        }
    }
}

서비스다.

리포지토리로부터 커넥션을 받아오고,

커넥션에 setAutoCommit(false)로 하여 오토커밋 모드를 껐다. 커넥션에 이렇게 직접 설정하면 바로 쿼리가 보내지나 보다.

 

그 다음 계좌이체(비즈니스) 로직을 실행했다.

 

트랜잭션 부분과 비즈니스 로직 부분을 나눈 것이다.

 

그냥 커넥션을 넘겨 받아, 그 커넥션으로 각 로직을 수행한다.

조회해서 각자 가지고 있던 금액을 알아오고, 이체할 돈 만큼 빼고, 더한다.

 

다시 트랜잭션 부분으로 돌아와서, 커밋을 해 준다. 커넥션에서 직접 커밋을 하면 커밋 쿼리가 보내지는 모양이다.

 

만약, 도중에 Exception이 터질 시 catch로 간다.

그래서 아예 롤백 해 버린다.

그럼 가장 최근에 commit된 상태로 돌아가 버린다.

 

그리고 마지막에는, 얻었던 커넥션의 오토커밋 모드를 켜주고, 

커넥션을 직접 닫아준다.

dataSource를 통한 커넥션에서 커넥션을 닫는다는 것은 커넥션을 실제로 닫는 것이 아니라, 커넥션 풀에다 이제 이 커넥션은 사용 가능하다. 라는 상태로 반환해 주는 것이다.

그래서 만약 저렇게 오토커밋 모드로 되돌려 주지 않을 시, 계속 그 상태로 커넥션(DB세션)이 남아있어 나중에 다른 사람이 커넥션 풀에서 커넥션을 획득 했을 때 이 커넥션을 획득하면, 오토커밋 모드가 꺼져있는 상태로 커넥션을 획득하게 된다.

 

그래서 꼭 꺼주자.

 

 

 

 

public class MemberServiceV2Test {
    private static final String MEMBER_A = "memberA";
    private static final String MEMBER_B = "memberB";
    private static final String MEMBER_EX = "ex";

    private MemberRepositoryV2 memberRepository;
    private MemberServiceV2 memberService;

    @BeforeEach
    private void before() throws SQLException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        memberRepository = new MemberRepositoryV2(dataSource);
        memberService = new MemberServiceV2(memberRepository);
    }

    @AfterEach
    private void after() throws SQLException {
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("정상 이체")
    void accountTransfer() throws SQLException {
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        int transferMoney = 2000;
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney);

        Member findedMemberA = memberRepository.findById(memberA.getMemberId());
        Member findedMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney() - transferMoney);
        assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney() + transferMoney);
    }

    @Test
    @DisplayName("이체 중 예외 발생")
    void accountTransferEx() throws SQLException {
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_EX, 10000);
        int transferMoney = 2000;
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        assertThatThrownBy(()->memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney))
                .isInstanceOf(IllegalStateException.class);

        Member findedMemberA = memberRepository.findById(memberA.getMemberId());
        Member findedMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney());
        assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney());
    }
}

테스트.

BeforeEach로 초기화,

AfterEach로 정리

 

정상 이체 부분은 같다. 커넥션을 얻고 같은 커넥션을 유지시키는 것도 Service 내부에서 하니 상관 없다.

 

이체 중 예외가 발생 했을 때는 좀 다르다.

 

원래 트랜잭션을 사용하기 전에는 오토커밋모드가 활성화 되어 있는 상태라 쿼리 날릴 때 마다 바로바로 DB에 반영이 되었는데,

 

이번에는 서비스 보면 중간에 예외가 터져버리면 아예 롤백하게끔 해서

DB에 변화가 있으면 안된다.

그래서 로직을 진행해도 이체 로직 전의 멤버와 이체 로직 후의 멤버가 이체 로직 중간에 예외가 발생했기 때문에 롤백되어 값이 똑같다.

'스프링 > 5. 스프링 DB-1' 카테고리의 다른 글

23. 트랜잭션 추상화  (0) 2023.09.30
22. 기존 트랜잭션의 문제  (0) 2023.09.30
20. 이체 테스트  (0) 2023.09.30
19. 조회할 때 락 가져오기  (0) 2023.09.30
18. DB 락  (0) 2023.09.29