Spring 강의/DB

[스프링 DB 1편] - (3) 트랜잭션

lxexjx 2022. 4. 20. 13:33

[트랜잭션] - 두개가 모두 성공해야 성공, 한번에 커밋& 롤백

 

원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 모두 실패

일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지

격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택

지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구

 


[DB연결 구조와 세션]

커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만듦

 


[트랜잭션 사용]

데이터 변경 쿼리를 실행하고 데이터베이스에 그 결과를 반영하려면 커밋 명령어인 commit 을 호출, 결과를 반영하고 싶지 않으면 롤백 명령어인 rollback 을 호출. 커밋을 호출하기 전까지는 임시로 데이터를 저장.

수동 커밋 모드로 설정하는 것 = 트랜잭션을 시작

 

[DB락]

세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막아야. , 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막아야됨

 


<MemberServieceV1Test>

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

    private MemberRepositoryV1 memberRepository;
    private MemberServiceV1 memberService;

    @BeforeEach
    void before() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,
                USERNAME, PASSWORD);    //memberRepository가 필요로 하니까 dataSource를 가져오고
        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);

        //whern
        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), 2000);

        //then -id가져오고
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findMemberA.getMoney()).isEqualTo(8000);
        assertThat(findMemberB.getMoney()).isEqualTo(12000);

    }

    @Test
    @DisplayName("이체중 예외 발생")
    void accountTransferException() throws SQLException {

        //given
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberEx = new Member(MEMBER_B, 10000);

        memberRepository.save(memberA);
        memberRepository.save(memberEx);

        //whern
        assertThatThrownBy(() ->
                memberA.getMemberId(), memberEx.getMemberId(), 2000).isInstanceOf(IllegalStateException.class);
        memberService.accountTransfer(memberA.getMemberId(), memberEx.getMemberId(), 2000);

        //then -id가져오고
        Member findMemberA = memberRepository.findById(memberA.getMemberId());
        Member findMemberB = memberRepository.findById(memberEx.getMemberId());

        assertThat(findMemberA.getMoney()).isEqualTo(8000);
        assertThat(findMemberB.getMoney()).isEqualTo(10000);
    }
}

 


커밋이날 롤배하면 트랜잭션 종료됨

(트랜잭션의 시작 : set Auto Commit = false)

 

 

<MemberRepositoryV2> - V1에 추가

/같은 파라미터를 사용해야 같은 커넥션 유지
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);  //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 {    //data가 없다
            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 {
            JdbcUtils.closeStatement(pstmt);
            //JdbcUtils.closeConnection(con);
        }
    }

 

 

<MemberServiceV2>

 

<MemberRepositoryV2Test>