김영한 님의 스프링 DB 1편 - 데이터 접근 핵심 원리 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard


1. 트랜잭션

1-1. 개념

  • DB에서 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다

  • 트랜잭션 기능을 사용하면 n개의 작업이 합쳐져서 하나의 작업처럼 동작을 해야하는 경우, n개의 작업이 모두 성공해야 DB에 반영하고, 하나라도 실패하면 작업 전의 상태로 돌아간다

  • 모든 작업이 성공해서 DB에 정상 반영하는 것을 커밋( Commit )이라 하고, 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백( Rollback )이라 한다


1-2. ACID

  • 트랜잭션은 ACID라 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다

  • Atomicity : 트랜잭션 내에서 실행한 여러 개의 작업들은 하나의 작업인 것처럼 전체적으로 수행되거나 전혀 수행되지 않아야한다

  • Consistency

    • 트랜잭션은 DB를 일관된 상태에서 일관된 상태로 변환해야한다

    • 즉, 모든 트랜잭션은 일관성 있는 DB 상태를 유지해야한다

  • Isolation

    • 트랜잭션은 서로 독립적으로 실행되며, 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 해야한다

    • ex> 동시에 같은 데이터를 수정하지 못하도록 해야한다

    • 동시성과 관련된 성능 이슈로 인해 트랜잭션 Isolation level을 선택할 수 있다

  • Durability

    • 성공적으로 끝난 트랜잭의 결과는 DB에 영구적으로 기록되며, 뒤의 트랜잭션의 실패로 인해 손실되면 안된다

    • 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다


1-3. Isolation level

  • 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 하는데 이렇게 하면 동시 처리 성능이 매우 나빠진다

  • 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의

    • READ UNCOMMITED( 커밋되지 않은 읽기 ) : 다른 곳에서 데이터를 변경 중이고 커밋하지 않았는데 그 데이터를 사용하는 것

    • READ COMMITTED( 커밋된 읽기 )

    • REPEATABLE READ( 반복 가능한 읽기 )

    • SERIALIZABLE( 직렬화 가능 )




2. DB 연결 구조와 DB 세션

  • 사용자는 웹 어플리케이션 서버( WAS )나 DB 접근 툴 같은 클라이언트를 사용해서 데이터베이스 서버에 접근 가능

  • 클라이언트는 DB 서버에 연결을 요청하고 커넥션을 맺게 되는데 이 때, DB 서버는 내부에서 세션을 만든다

  • 앞으로 해당 커넥션을 이용한 모든 요청은 생성된 세션을 통해 실행된다

  • 즉, 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다

  • 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다

  • 트랜잭션 종료 이후에 새로운 트랜잭션을 다시 시작할 수 있다

  • 사용자가 커넥션을 닫거나, DBA( DB 관리자 )가 세션을 강제로 종료하면 세션은 종료된다

  • 커넥션 풀이 커넥션을 생성하는 커넥션만큼 세션도 동일한 개수만큼 생성된다




3. 트랜잭션 commit

  • ( 수동커밋의 경우 ) 쿼리를 실행하고 DB에 결과를 반영하려면 commit 을 호출하고, 결과를 반영하고 싶지 않으면 rollback 을 호출

  • ( 수동커밋의 경우 ) 커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이기 때문에 해당 트랜잭션을 시작한 세션( 사용자 )에게만 변경 데이터가 보이고 다른 세션( 사용자 )에게는 변경 데이터가 보이지 않는다

  • Isolation level을 READ UNCOMMITED로 설정하면 임시로 저장되는 데이터가 다른 세션( 사용자 )에게 보여지게 된다




4. 자동 커밋과 수동 커밋

  • 자동 커밋 : 각 쿼리 실행 직후에 자동으로 커밋을 호출하기 때문에 커밋이나 롤백을 호출하지 않아도 DB에 반영된다

  • 수동 커밋 : 쿼리 이후에 커밋이나 롤백을 직접 호출해주어야 한다

  • 자동 커밋의 문제점

    • ex> 계좌이체를 생각했을 때 A에서 2000원을 빼는 쿼리가 성공하고 B에서 2000을 더하는 쿼리가 실패했다면

    • 자동 커밋인 경우, B는 변화가 없고 A에서만 2000원이 빠지는 문제가 발생한다

  • 자동 커밋이나 수동 커밋 모드는 한 번 설정하면 해당 세션에서는 계속 유지되며, 중간에 변경하는 것이 가능하다

    • set autocommit = true ( false )

    • 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현한다




5. DB Lock - 변경

  • 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안 커밋이나 롤백을 호출하기 전까지 다른 세션에서 해당 데이터를 수정할 수 없도록 막는 것이 DB Lock
  1. 세션 1 ( 1번, 2번, 3번 )

    • 트랜잭션을 시작하고 데이터를 수정하려면 먼저 수정하려는 데이터가 있는 row의 lock을 획득해야한다

    • lock을 획득하면 해당 row에 update 쿼리를 수행할 수 있다

  2. 세션 2 ( 4번, 5번 )

    • 데이터를 수정하려고 하는데 row에 lock이 없는 경우, lock이 돌아올 때까지 대기한다

    • lock 대기 시간을 넘어가면 lock 타임아웃 오류가 발생 ( 대기 시간은 설정 가능 )

    • ex> SET LOCK_TIMEOUT 60000;

  3. 세션 1 ( 세션 1의 6번 )

    • 커밋을 수행하면 트랜잭션이 종료되었으므로 lock을 반납
  1. 세션 2 ( 세션 2의 6번, 7번, 8번 )

    • 대기하던 세션이 lock을 획득 후 update 쿼리를 수행하고 마찬가지로 commit 후 lock을 반납한다



6. DB Lock - 조회

  • 보통 데이터를 조회할 때는 lock을 획득하지 않고 바로 데이터를 조회할 수 있다

  • 세션 1에서 데이터를 수정하고 있어도 세션 2에서 조회가 가능한데 이 때 조회되는 데이터는 세션1이 수정하기 전의 데이터

  • select ~~~ for update를 사용하면 조회할 때도 lock을 획득하게 된다

    • ex> select * from member where member_id='memberA' for update;
  • 조회 시점에 lock을 획득해야 하는 경우

    • 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용

    • ex> 어플리케이션 로직에서 금액을 조회한 다음 이 금액 정보로 어플리케이션에서 계산을 수행하는 경우, 계산을 완료할 때 까지 조회한 금액을 다른곳에서 변경하면 안되기 때문에 조회 시점에 lock을 획득하면 된다




7. 트랜잭션 적용

7-1. 설명

  • 어플리케이션에서 트랜잭션은 비지니스 로직이 있는 서비스 계층에서 시작해야한다

    • 정확하게는 비지니스 로직을 수행하는 메서드 안에서 시작

    • 비지니스 로직이 잘못되면 해당 비지니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야하기 때문

  • 트랜잭션을 시작하려면 커넥션이 필요하기 때문에 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료한다

  • 어플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 같은 세션을 사용할 수 있다

    • ex> A의 잔액을 차감하고 B의 잔액을 늘리는 경우, 같은 커넥션을 사용해야한다
  • ➡️커넥션을 파라미터로 전달해서 같은 커넥션을 사용하도록한다

  • ➡️즉, Service 계층에서 생성한 커넥션을, 데이터를 변경하는 Repository의 메서드를 수행할때 파라미터로 넘겨준다


7-2. 코드 수정

7-2-1. Repository

public class MemberRepositoryV2 {
    // 파라미터에 Connection 추가
    public Member findById(Connection con, String memberId) throws SQLException {

        // Connection con = null; ➜ 제거
        ...

        try {
            // con = getConnection();  ➜ 제거
            ...

        } catch (SQLException e) {
            ...
        } finally {
            // close(con, pstmt, rs);  ➜ 제거
            JdbcUtils.closeResultSet(rs);   // 추가
            JdbcUtils.closeStatement(pstmt);    // 추가
        }
    }
}
  • 동일한 커넥션을 사용해야하기 때문에 파라미터에 Connection 추가

  • 메서드에서 Connection 선언하는 부분과 getConnection() 제거

  • Connection은 닫으면 안되기 때문에 메서드를 이용해서 한 번에 닫지 않고 다른 리소스들을 닫는 코드 추가


7-2-2. Service

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 static void release(Connection con) {
        if (con != null) {
            try {
                con.setAutoCommit(true);    // 커넥션 풀 고려
                con.close();
            } catch (Exception e) {
                log.info("error", e);
            }
        }
    }
}
  • 메서드를 수행할 때 getConnection()을 통해 Connection을 획득

  • 트랜잭션 시작 전, setAutoCommit(false)로 수동 커밋 모드 설정

  • 정상적으로 처리된 경우, commit()호출, 예외가 발생하면 rollback()호출

  • con.close()를 하면 커넥션 풀에 돌아가는데 처음에 수동 커밋 모드로 설정을 했으니 설정이 유지된 상태로 커넥션 풀에 돌아감

  • 보통 자동 커밋 모드로 기본값이 설정되어 있기 때문에 close()를 호출하기 전에 자동 커밋 모드로 변경해준다




8. 문제점

  • 서비스 계층에서 비지니스 로직 + 트랜잭션 처리를 하기 때문에 코드가 지저분하다

  • 트랜잭션 처리 = 비지니스 로직 수행 전, 후에 커밋 모드 설정과 commit / rollback 처리

  • 또한 Repository에 동일한 기능을 수행하지만 Connection을 생성해서 사용하는 메서드와 동일한 Connection을 사용하는 메서드와 두 개가 존재하게 된다

0개의 댓글

Powered by GraphCDN, the GraphQL CDN