[Spring] JDBC Transaction

Manx·2022년 7월 7일
0

spring

목록 보기
21/24

인프런 '스프링 DB 1편' - 김영한님의 강의 내용입니다.

트랜잭션의 개념에 대한 설명은 다른 포스트에서 했으므로 생략하겠다.
JDBC를 통해 AutoCommit을 끈 뒤 트랜잭션을 처리하는 부분을 정리하고자 한다.

비즈니스 로직에 따라 커밋하거나 롤백해야 하므로 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야한다.
가장 간단한 방법은 커넥션을 파라미터로 전달해 같은 커넥션이 사용되도록 유지하는 것이다.

Repository

Repository의 비즈니스 로직을 수행하는 메서드들의 파라미터로 Connection을 넣는다.
여기서 주의할 점은 같은 커넥션을 사용해야 하므로 각 메서드들에서 Connection를 릴리즈하지 않아야 한다.

@Slf4j
public class MemberRepositoryV1 {

    private final DataSource dataSource;

    public MemberRepositoryV1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

   	// ... save
    
    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) {
            log.error("db error", e);
            throw e;
        } finally {
            // 커넥션 닫지 않음 주의!
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
        }
    }

  
public void update(Connection con ,String memberId, int money) throws SQLException {
        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 resultSize = pstmt.executeUpdate();
            log.info("resultSize={}", resultSize);

        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            JdbcUtils.closeStatement(pstmt);
        }

    }
}

Service

MemberService에서 AutoCommit을 끈 뒤 비즈니스 로직에 문제가 생겼을 경우 롤백하고, 아니면 커밋되도록 한다.
Connection을 반납할 경우 연결을 끊는 것이 아닌 Connection Pool에 반납하는 것이므로 AutoCommit을 다시 켜준 뒤 반납해야 한다.

/**
 * 트랜잭션 - 파라미터랑 연동, 커넥션 풀 사용
 */
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {

    private final DataSource dataSource;
    private final MemberRepositoryV1 memberRepository;

    public void accountTransfer(String formId, String toId, int money) throws SQLException {
        Connection con = dataSource.getConnection();

        try {
            con.setAutoCommit(false); // 트랜잭션 시작
            bizlogic(con, formId, toId, money);
            con.commit(); // 성공시 커밋
        } catch (Exception e) {
            con.rollback(); // 실패시 롤백
            throw new IllegalStateException(e);
        } finally {
            release(con);
        }
    }

    private void bizlogic(Connection con, String formId, String toId, int money) throws SQLException {
        // 비즈니스 로직
        Member fromMember = memberRepository.findById(con, formId);
        Member toMember = memberRepository.findById(con, toId);

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

    private void release(Connection con) {
        if (con != null) {
            try {
                con.setAutoCommit(true); //커넥션 풀 고려
                con.close();
            } catch (Exception e) {
                log.info("error", e);
            }
        }
    }

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

TestCode

userName이 EX라 IllegalStateException이 터질 것이고, memberA와 memberEX 모두 10000 으로 롤백될 것이다.

@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
    assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEX.getMemberId(), 2000))
    										.isInstanceOf(IllegalStateException.class);

    //then
    Member findMemberA = memberRepository.findById(memberA.getMemberId());
    Member findMemberB = memberRepository.findById(memberEX.getMemberId());
    assertThat(findMemberA.getMoney()).isEqualTo(10000);
    assertThat(findMemberB.getMoney()).isEqualTo(10000);
}

비즈니스로직을 처리하는 것보다 트랜잭션을 처리하는 과정이 더욱 길다.
또한 트랜잭션을 처리하기 위해 Service 계층이 JDBC 구현 기술을 의존하고 있어, DB 접근 기술을 바꿀 시 Service 계층의 코드를 변경해야 한다. 이런 문제점들을 다음 강의부터 해결해 나아간다고 한다!

profile
백엔드 개발자

0개의 댓글