[Spring DB] 3.트랜잭션과 락

Updated:

트랜잭션과 락

DB가 제공하는 가장 중요한 기능인 트랜잭션과 락에 대해 알아본다

1. 트랜잭션

데이터 저장을 파일이 아닌 DB에 하는 이유를 묻는다면 트랜잭션 지원을 1순위로 꼽을 것이다

1-1. 트랜잭션이란

  • 하나의 서비스 작업을 안전하게 처리하도록 보장해주는 것을 의미한다
  • 예를 들어, A가 B에게 5000원을 계좌이체하는 상황을 가정하자. 이 경우에 2가지가 수행되어야 한다
    1. A의 잔고가 5000원 감소
    2. B의 잔고가 5000원 증가
  • 위 2가지가 동시에 수행되어야 계좌이체가 온전히 동작했다고 볼 수 있는데, 둘 중 하나라도 실패하면 DB는 문제가 발생했다고 여기고 거래 이전으로 되돌리는데 이를 Rollback이라 부른다
  • 두가지 작업이 모두 성공하여 DB에 정상반영되는 것을 Commit이라 부른다

1-2. 트랜잭션 ACID

트랜잭션이 보장해야 하는 4가지 항목이다

  1. 원자성(Atomicity)
    • 트랜잭션 내에서 실행한 작업들은 모두 성공하거나 모두 실패해야 한다
  2. 일관성(Consistency)
    • 모든 트랜잭션은 무결성 제약조건 등 일관성 있는 DB 상태를 유지해야 한다
  3. 격리성(Isolation)
    • 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다
    • 예를 들어, 동시에 같은 데이터를 수정하지 못하도록 해야한다
  4. 지속성(Durability)
    • 트랜잭션이 성공적으로 마무리되면 로그 등을 통해 성공한 결과가 항상 기록되야 한다

트랜잭션 간에 격리성을 완전히 보장하려면 각각의 트랜잭션 실행순서를 정해 순서대로 실행하는 수밖에 없다. 단, 이렇게 하면 동시 처리 성능이 매우 나빠져서 ANSI 표준은 격리수준을 4단계로 나눠 정의한다. 밑으로 내려갈수록 성능은 느려지는 대신 격리성은 확실히 보장된다

  1. READ UNCOMMITED(커밋되지 않은 읽기)
  2. READ COMMITED(커밋된 읽기)
  3. REPEATABLE READ(반복 가능한 읽기)
  4. SERIALIZABLE(직렬화 가능)

주로 READ COMMITED(커밋된 읽기)를 사용하며 때에 따라 REPEATABLE READ(반복 가능한 읽기)까지 사용한다

1-3. DB 연결구조와 DB 세션

jdbc1

  • 사용자가 WAS나 DB 접근 툴을 이용하여 DB Server에 접근하여 Connection을 맺는다
  • DB Server는 내부에 세션을 만들어 이후부터 모든 작업은 세션을 통해 수행된다
  • 세션은 트랜잭션을 시작하고, COMMIT 혹은 ROLLBACK한 후 트랜잭션을 종료한다
  • 사용자가 Connection을 닫으면 세션도 자동으로 종료된다
  • 여러 Clinet가 커넥션 풀에서 Connection을 가져와 연결할 경우 해당 개수만큼 세션도 생성된다

1-4. 트랜잭션 동작

  • 데이터 변경 쿼리를 실행한 후에 결과를 반영하려면 COMMIT을, 반영하지 않으려면 ROLLBACK을 호출한다
  • COMMIT을 호출하기 전까지는 임시로 데이터가 저장되어 해당 트랜잭션을 수행한 사용자가 아닌 이상 해당 데이터가 보이지 않는다

아래 예시를 통해 자세히 살펴보자

jdbc2

  • 세션1과 세션2 모두 테이블 조회시에 가운데 테이블의 데이터가 조회된다

jdbc3

  • 세션1이 트랜잭션 내부에서 변경 작업을 수행하고 COMMIT하기 이전이라면 임시 데이터 상태이므로 세션2는 조회가 불가능하다
  • COMMIT 이전의 데이터를 해당 트랜잭션을 수행한 사용자 이외에서 접근가능하다고 가정해보자. 그러면 다른 사용자가 해당 데이터에 대해 작업을 수행해도 원래 사용자가 ROLLBACK을 호출하는 순간 작업이 사라지는 데이터 정합성의 문제가 발생한다. 그래서 COMMIT 시점 이전의 데이터는 다른 사용자가 조회할 수 없도록 설정한다
  • 트랜잭션 내부에서 변경 작업 수행후 COMMIT하면 실제 DB에 반영되고, ROLLBACK하면 원래 DB 상태로 복구된다

1-5. 자동커밋과 수동커밋

set autocommit true;	//자동커밋 설정
set autocommit false;	//수동커밋 설정
  • 자동커밋으로 설정하면 변경 작업 수행시마다 COMMIT이 수행되므로 자동으로 DB에 변경사항이 적용된다
  • 수동커밋으로 설정하면 변경 작업을 수행해도 직접 COMMIT하기 전까지는 DB에 변경사항이 적용되지 않는다

1-6. 실무에서의 트랜잭션

그렇다면 트랜잭션은 어디서 시작하고, 어디서 커밋해야할지에 대한 의문점이 생긴다. 어떤 계층에서 트랜잭션을 걸어야 할지에 대해 알아보자

결론부터 말하자면 비즈니스 로직이 있는 서비스 계층에 걸어야 한다

jdbc4

  • 비즈니스 로직에 에러가 발생하는 등 문제가 발생하면 해당 로직 전체를 롤백해야 하기 때문에 비즈니스가 시작하는 시점부터 끝나는 지점까지 하나의 트랜잭션에 포함되어야 한다
  • 서비스 계층에서 커넥션을 만들어 트랜잭션을 시작하고 커밋 이후에 커넥션을 종료한다
  • 하나의 트랜잭션 내에서는 같은 커넥션을 유지해야 한다
    • 다른 커넥션을 이용하는 순간 다른 세션을 사용하게 된다

2. 락

2-1. 락이란

2개의 세션이 동일한 트랜잭션 내에서 같은 데이터를 수정하고자 하면 트랜잭션의 원자성이 보장되지 않는다. 이를 막기 위해 락이라는 개념을 도입한다

그림을 통해 이해하기 쉽게 알아보자

jdbc5

  • 같은 트랜잭션 내에서 세션1과 세션2가 동일하게 memberAmoney값을 수정하려고 한다
  • 세션1이 memberAmoney값을 수정하는 동안 세션2가 동일한 값을 수정하는 경우가 발생해선 안된다
  • 이를 막기 위해 락이라는 개념이 도입되었다

jdbc6

  • 이 예시에서 세션1의 요청이 세션2보다 빨랐다고 가정한다
  • 세션1은 트랜잭션을 시작하고 money값의 변경을 시도하는데 이 과정에서 락을 획득해야 한다
  • 세션1은 락을 획득한 후 UPDATE SQL문을 수행한다

jdbc7

  • 동시에 세션2도 트랜잭션을 시작하고 money값을 변경하고자 한다
  • 락이 존재하지 않기 때문에 세션1이 락을 반납할때까지 기다린다
    • 락이 돌아올 때까지 무한정 기다리지 않고 일정 대기시간이 지난 후에는 타임아웃 오류가 발생한다
    • 트랜잭션이 종료되면 세션1은 락을 반납한다

2-2. 조회시의 락

  • 동일한 트랜잭션 내에서 동일한 데이터에 대해 세션1이 수정 중이면 세션2는 수정 작업을 수행할 수 없다
  • 하지만, 세션1이 수정 작업을 진행 중이어서 락을 가지고 있는 상황이더라도 세션2가 일반적인 조회는 수행할 수 있다
  • 필요하다면 조회 시에도 락을 가져와서 다른 세션에서 수정 작업을 수행하지 못하도록 막을 수 있다
    • select for update문을 사용하면 된다
    • 실제로 금액 조회 이후에 해당 금액을 이용하여 중요한 계산을 수행하는 경우에는 다른 세션에서 수정하지 못하도록 막아야 한다. 이 경우에 조회시에도 락을 사용한다

Leave a comment