트랜잭션, 락

바그다드·2023년 3월 25일
0
post-thumbnail

트랜잭션

데이터베이스의 상태를 변화시키기 해서 수행하는 작업의 단위

  • 하나의 트랜잭션에는 여러 작업이 동시에 이뤄질 수도 있는데 같은 트랜잭션에 묶인 작업들은 모두가 성공(commit)하거나 모두가 실패(roll back)해야 함
  • 예를 들어,
    a가 b에게 500원을 준다고 했을 때를 하나의 트랜잭션으로 보자면
  1. a의 소유금을 500원 차감
  2. b의 소유금을 500원 증가
    위의 1,2가 모두 성공하거나 모두 실패해야 한다.

ACID

  • 트랜잭션은 ACID를 보장해야 하는데
  1. 원자성
    • 트랜잭션의 모든 작업은 모두 성공하거나 모두 실패해야 한다
  2. 일관성
    • 모든 트랜잭션은 일관성 있는 db상태를 유지해야 함
    • 예를 들어 무결성 제약 조건을 항상 만족하는 것
  3. 격리성
    • 각 트랜잭션은 서로에게 영향을 미치지 않고 격리되어 수행해야함
  4. 지속성
    • 트랜잭션이 성공적으로 끝났다면 그 결과는 항상 유지되어야 함
    • 시스템 장애가 발생하더라도 결과는 적용되어야 함

DB연결 구조

  • 클라이언트가 DB에 연결 요청을 하고 커넥션을 맺으면, DB서버 내부에서 세션을 생성
  • 세션이 SQL을 실행
  • 커넥션 하나당 세션 하나가 필요하며 커넥션이 닫히면 세션도 함께 사라진다.

DB락

트랜잭션이 수행되는 동안 해당 리소스에는 접근하지 못하도록 막는 것

  • 트랜잭션 수행 도중에 다른 트랜잭션이 동시에 같은 데이터를 수정하면 문제가 발생할 수 있음
  • A의 트랜잭션이 먼저 시작되면 B는 A의 트랜잭션이 끝날 때까지 대기해야 함

조회 락

  • 일반적으로 select에는 락을 사용하지 않지만, for update 구문을 사용하면 락을 획득할 수 있음
  • 예를들어, 현재 통장 잔액을 기반으로 하는 로직이 필요할 때는 계산이 완료될 때까지 데이터가 변경되어서는 안됨
select * from member where member_id='memberA' for update;

트랜잭션 적용

// 리포지토리
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 {
            // connection은 여기서 닫지 않는다.
            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);
        }

    }
// 서비스
public void accountTrancefer(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);
    }
  • 서비스 로직에서 커넥션을 얻어 로직을 실행한 후, 서비스 로직에서 커넥션을 닫음
  • 이를 통해 트랜젝션 원자성을 지킬 수 있게 됨!!!
  • 근데 서비스로직이 지저분해진다는 문제가 있음
profile
꾸준히 하자!

0개의 댓글