트랜잭션 적용

Minseo Kang·2023년 5월 9일
0
post-thumbnail

01. 비즈니스 로직과 트랜잭션


트랜잭션은 비즈니스 로직이 시작하는 부분에서 시작되어야 한다. 비즈니스 로직이 잘못되면 문제가 되는 부분을 함께 롤백해야 하기 때문이다. 트랜잭션을 시작하려면 커넥션이 필요하고, 트랜잭션을 사용하는 동안 같은 세션을 사용하기 위해 같은 커넥션을 유지해야한다.

결론적으로, 서비스 계층에서 커넥션을 만들고, 해당 커넥션은 같은 커넥션을 유지해야 하며, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.

애플리케이션에서 같은 커넥션을 유지하기 위해서 커넥션을 파라미터로 전달하면 된다.




02. 리포지토리에서 파라미터 추가 - MemberRepositoryV2 클래스


  • 트랜잭션 동안 같은 커넥션을 유지하기 위한 목적으로 작성하였다.
  • 커넥션은 파라미터로 넘어온 커넥션을 사용해야 한다.
  • 커넥션 유지를 위해 리포지토리에서 커넥션을 닫으면 안된다. 서비스 로직이 끝날 때 트랜잭션을 종료하고 닫아야 한다.
  • 다음 두 메소드는 계좌이체 서비스 로직에서 호출하는 메서드이다.
public Member findById(Connection con, String memberId) throws SQLException {

	String sql = "select * from member where member_id = ?";

	// Connection con = null; // 삭제
	PreparedStatement pstmt = null;
	ResultSet rs = null;

	try {
		// con = getConnection(); // 삭제 (이것 사용하면 새로운 커넥션이 맺어짐)
		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 {
		// *** connection 은 여기에서 닫지 않는다 ***
		JdbcUtils.closeResultSet(rs);
		JdbcUtils.closeStatement(pstmt);
		// JdbcUtils.closeConnection(con); // 서비스 계층에서 커넥션 종료해야한다
	}
}
    
public void update(Connection con, String memberId, int money) throws SQLException {

	String sql = "update member set money=? where member_id=?";

	// Connection con = null; // 삭제
	PreparedStatement pstmt = null;
        
	try {
		// con = getConnection(); // 삭제
		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 {
		// *** connection 은 여기에서 닫지 않는다 ***
    	JdbcUtils.closeStatement(pstmt);
		// JdbcUtils.closeConnection(con); // 서비스 계층에서 커넥션 종료해야 한다
	}
}



03. 트랜잭션 연동 로직 작성 - MemberServiceV2 클래스


package hello.jdbc.service;

// transaction - 파라미터 연동, 풀을 고려한 종료

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV1;
import hello.jdbc.repository.MemberRepositoryV2;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@Slf4j
@RequiredArgsConstructor
public class MemberServiceV2 {

    private final DataSource dataSource;
    private final MemberRepositoryV2 memberRepository;

    // 계좌이체 로직
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {

        // 비즈니스 로직 시작할 때 커넥션 얻어야 함
        Connection con = dataSource.getConnection();
        try {
            con.setAutoCommit(false); // 트랜잭션 시작

            bizLogic(con, fromId, toId, money); // 비즈니스 로직

            con.commit(); // 정상적으로 실행되었다면 커밋

        } catch (Exception e) { // 예외 발생 (실패) 시 롤백
            con.rollback();
            throw new IllegalStateException(e);
        } finally { // 안전하게 종료
            release(con);
        }

    }

    private void bizLogic(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 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("이체중 예외 발생");
        }
    }

}



04. 테스트 코드 작성 - MemberServiceV2Test 클래스


package hello.jdbc.service;

// transaction - 커넥션 파라미터 전달 방식 동기화 (같은 커넥션 사용)

import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV1;
import hello.jdbc.repository.MemberRepositoryV2;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import java.sql.SQLException;

import static hello.jdbc.connection.ConnectionConst.*;

@Slf4j
class MemberServiceV2Test {

    public static final String MEMBER_A = "memberA";
    public static final String MEMBER_B = "memberB";
    public static final String MEMBER_EX = "ex";

    private MemberRepositoryV2 memberRepository;
    private MemberServiceV2 memberService;

    @BeforeEach
    void before() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        memberRepository = new MemberRepositoryV2(dataSource);
        memberService = new MemberServiceV2(dataSource, 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 : 이런 상황이 실행되었을 때
        log.info("START TX"); // 로그를 통해 같은 커넥션이 유지되는 것을 확인할 수 있음
        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);
        log.info("END TX");

        // then : 이것을 검증한다
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());
        Assertions.assertThat(findMemberA.getMoney()).isEqualTo(8000);
        Assertions.assertThat(findMemberB.getMoney()).isEqualTo(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.assertThatThrownBy(() -> memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000))
                .isInstanceOf(IllegalStateException.class);

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

}

0개의 댓글