Spring/DB

[Spring/DB] 03. 트랜잭션 이해

lumana 2025. 3. 25. 00:04

 

3. 트랜잭션 이해


#Spring/DB

정리

트랜잭션 - 개념 이해

  • 파일이 아닌 데이터베이스에 데이터를 저장하는 이유 트랜잭션
  • 하나의 거래를 안전하게 처리하도록 보장
  • 커밋(Commit): 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것
  • 롤백(Rollback): 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것

트랜잭션 ACID

  • 트랜잭션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 함
    • 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
    • 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다.
    • 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.
    • 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.
  • 트랜잭션은 원자성, 일관성, 지속성을 보장
  • 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해함 성능이 나빠짐 격리 수준을 나눔

트랜잭션 격리 수준 - Isolation level

  1. READ UNCOMMITED (커밋되지 않은 읽기)
  2. READ COMMITTED (커밋된 읽기)
  3. REPEATABLE READ (반복 가능한 읽기)
  4. SERIALIZABLE (직렬화 가능)
  • 단계가 높아질 수록 속도가 느려짐. 일반적으로 2, 3번 사용.
  • 이 강의는 READ COMMITTED 기준으로 설명

데이터베이스 연결 구조와 DB 세션

  • WAS 등을 통해 DB와 연결을 맺으면 DB에 세션이 만들어지고, 커넥션의 요청은 세션을 통해 실행됨
    • 세션이 SQL을 실행한다.
    • 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료
  • 사용자가 커넥션을 닫거나, 또는 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료됨
  • 커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만들어진다.

트랜잭션 사용법 - 데이터 추가 트랜잭션

  • 커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이다. 따라서 해당 트랜잭션을 시작한 세션(사용자)에게만 변경 데이터가 보이고 다른 세션(사용자)에게는 변경 데이터가 보이지 않는다.
  • 만약 커밋하지 않은 데이터를 다른 곳에서 조회할 수 있다면 어떤 문제가 발생? (READ UNCOMITTED)
    • 조회 후 로직 수행하고 있는데 다른 세션에서 롤백해버리면 데이터 정합성에 큰 문제가 발생

트랜잭션 - DB 예제2 - 자동 커밋, 수동 커밋

  • 자동 커밋: 각각의 쿼리 실행 직후에 자동으로 커밋을 호출. 트랜잭션이 가져다주는 기능 사용할 수 없음.
    • 커밋, 롤백 이런 거 쓰려면 수동 커밋 사용해야 함.
    • 자동 커밋 설정: set autocommit true
    • 수동 커밋 설정: set autocommit false
    • 보통 자동 커밋 모드가 기본으로 설정된 경우가 많기 때문에, 수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현할 수 있다.
    • 한 번 설정하면 세션 중에 유지되고, 중간에 변경하는 것도 가능함.

트랜잭션 - DB 예제3 - 트랜잭션 실습: 단순 데이터 추가 예제

  • 커밋을 하든 롤백을 하든 문제 발생 X

트랜잭션 - DB 예제4 - 계좌이체

  • 계좌이체 정상 상황
    • 세션 1에서 memberA 2000원 빼고, memberB 2000원 더한 뒤 커밋하면 성공
  • 계좌이체 문제 상황 - 커밋
    • SQL에 문제가 있어 memberA의 돈을 2000원 줄이는 것에는 성공했지만, memberB의 돈을 2000원 증가시키는 것에 실패한다.
    • 이 때 강제로 커밋하면 memberA의 돈만 줄어드는 문제가 발생한다.
  • 계좌이체 문제 상황 - 롤백
    • 중간에 문제가 발생했을 때 롤백을 호출해서 트랜잭션 시작 시점으로 데이터를 원복해야한다.
  • 정리
    • 원자성: 작업은 모두 성공하거나 실패해야 한다. 트랜잭션 덕분에 가능함을 확인했다.
    • 오토 커밋: 오토 커밋 모드에서 계좌이체 중간에 실패하면 memberA의 돈만 줄어드는 문제가 발생한다.
    • 수동 커밋을 통해 커밋, 롤백할 수 있도록 하자.

DB 락 - 개념 이해

  • 세션 1이 데이터 수정하는 동안 세션2가 데이터 동시에 수정하면 여러 문제가 발생한다. 대표적으로 원자성이 깨진다.
  • 세션 1이 심지어 롤백까지 해버리면 세션 2는 잘못된 데이터를 수정하는 작업을 하게 된다.
  • 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막자.

DB 락 - 변경

  1. 수동 커밋으로 설정
  2. SET LOCK_TIMEOUT 60000 락 획득 시간 설정.
    • 시간 지나면 락 타임아웃 오류가 발생
  3. 세션 1이 락 획득
  4. 세션 2는 락 획득 대기
  5. 세션 1이 커밋 후 락 반납
  6. 세션 2는 락 획득
  7. 세션 2가 커밋 후 락 반납

DB 락 - 조회

일반적인 조회는 락을 사용하지 않는다

데이터베이스마다 다르지만, 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 데이터를 조회할 수 있다.


조회와 락

  • 데이터를 조회할 때도 락을 획득하고 싶을 때는 select for update 구문을 사용.
    • 조회 시점에 락을 가져가버리기 때문에 다른 세션에서 해당 데이터를 변경

조회 시점에 락이 필요한 경우는 언제일까?

  • 트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용
    • 정산 로직과 같이 조회한 정보를 바탕으로 로직을 수행하는 경우
  • select for update 구문을 사용하면 조회를 하면서 동시에 선택한 로우의 락도 획득한다.

트랜잭션 - 적용1

@RequiredArgsConstructor
public class MemberServiceV1 {
    private final MemberRepositoryV1 memberRepository;

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);
        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private void validation(Member toMember) {
        if (toMember.getMemberId().equals("ex")) {
            throw new IllegalStateException("이체중 예외 발생");
        }
    }
}
  • fromId의 회원을 조회해서 toId의 회원에게 money만큼의 돈을 계좌이체하는 로직
    • 트랜잭션 없이 로직을 작성한 예제
  • memberA memberEx로 2000원 계좌이체 한다.
    • memberA의 금액이 2000원 감소한다.
    • memberEx 의 금액을 업데이트 하기 전에 예외가 발생한다
    • 계좌이체는 실패한다. memberA의 돈만 2000원 줄어든다.

트랜잭션 - 적용2

  • DB 트랜잭션을 사용해서 앞서 발생한 문제점을 해결해보자.

비즈니스 로직과 트랜잭션

  • 트랜잭션은 비즈니스 로직이 있는 서비스 계층에서 시작해야 한다.
    • 비즈니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야 하기 때문
  • 트랜잭션을 시작하려면 커넥션이 필요하다. 결국 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.
  • 애플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 한다. 그래야 같은 세션을 사용할 수 있다.

커넥션과 세션

  • 애플리케이션에서 같은 커넥션을 유지하려면
    • 파라미터로 같은 커넥션을 넘겨줘야 한다.
    • 커넥션 유지가 필요한 메서드는 리포지토리에서 커넥션을 닫으면 안 된다.
    • 서비스 로직이 끝날 때 트랜잭션을 종료하고 닫아야 한다.
  1. Connection con = dataSource.getConnection(); 로 커넥션 획득
  2. con.setAutoCommit(false); //트랜잭션 시작
  3. bizLogic(con, fromId, toId, money);
    • 트랜잭션 관리 로직과 실제 비즈니스 로직을 구분해주자.
  4. con.commit(); //성공시 커밋
  5. con.rollback(); //실패시 롤백
  6. release(con);
    • 커넥션을 모두 사용하고 나면 안전하게 종료한다.
    • 실제로는 커넥션이 종료되는게 아니라 커넥션 풀에 반납되기 때문에, 기본 값인 자동 커밋 모드로 변경해주자.
  • 결과: 계좌이체 실패 시 트랜잭션 덕분에 데이터를 정상적으로 초기화하였다.

남은 문제

애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해지고, 생각보다 매우 복잡한 코드를 요구한다.

  • 커넥션 유지 등

참고) 스프링 DB 1편 - 데이터 접근 핵심 원리 강의 | 김영한 - 인프런