[DB] 트랜잭션이 뭐예요?

kshired·2022년 1월 25일
0

트랜잭션(Transaction)이란 무엇인가?

트랜잭션은 작업의 완전성 을 보장해주는 것이다.

즉, 논리적인 작업 셋을 모두 완벽하게 처리하거나 또는 처리하지 못할 경우에는 원 상태로 복구해서 작업의 일부만 적용되는 현상이 발생하지 않게 만들어주는 기능이다. 사용자의 입장에서는 작업의 논리적 단위로 이해를 할 수 있고 시스템의 입장에서는 데이터들을 접근 또는 변경하는 프로그램의 단위가 된다.

트랜잭션과 Lock

잠금(Lock)과 트랜잭션은 서로 비슷한 개념 같지만 사실 잠금은 동시성을 제어하기 위한 기능이고 트랜잭션은 데이터의 정합성을 보장하기 위한 기능이다.

잠금은 여러 커넥션에서 동시에 동일한 자원을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 한다. 여기서 자원은 레코드나 테이블을 말한다.

이와는 조금 다르게 트랜잭션은 꼭 여러 개의 변경 작업을 수행하는 쿼리가 조합되었을 때만 의미있는 개념은 아니다.

트랜잭션은 하나의 논리적인 작업 셋 중 하나의 쿼리가 있든 두 개 이상의 쿼리가 있든 관계없이 논리적인 작업 셋 자체가 100% 적용되거나 아무것도 적용되지 않아야 함을 보장하는 것이다. 예를 들면 HW 에러 또는 SW 에러와 같은 문제로 인해 작업에 실패가 있을 경우, 특별한 대책이 필요하게 되는데 이러한 문제를 해결하는 것이다.

Shared Lock

Read Lock라고도 하는 공유락은 데이터를 읽을 때 사용하는 Lock.

Read Lock은 같은 Read Lock 끼리는 동시에 접근이 가능하다. Database의 주요 기능인 데이터 일관성과 무결성을 해치지 않기 때문이다. 사용자가 데이터를 읽어 갈 뿐, 데이터 변경이 없기 때문에 가능.

대신 그 다음에 나올 Exclusive Lock의 접근을 막습니다.

Exclusive Lock

Write Lock이라고도 하는 베타락은 데이터를 변경할 때 사용하는 Lock.

트랜잭션이 완료될 때까지 유지된다. Exclusive Lock이 끝나기 전까지 어떠한 접근도 허용하지 않음. 이 Lock은 다른 트랜잭션이 수행되고 있는 데이터에 대해서 접근하여 Lock을 걸 수 없다.

Shared Lock, Exclusive Lock 특징 정리

  • 여러 트랜잭션이 동시에 한 Row에 Shared Lock을 걸 수 있다. 즉, 여러 트랜잭션이 동시에 한 Row를 읽을 수 있다.

  • Shared Lock이 걸려있는 Row에 다른 트랜잭션이 Exclusive Lock을 걸 수 없다. 즉, 다른 트랜잭션이 읽고 있는 Row를 수정하거나 삭제할 수 없다.

  • Exclusive Lock이 걸려있는 Row에는 다른 트랜잭션이 Shared Lock, Exclusive Lock 둘 다 걸 수 없다. 즉, 다른 트랜잭션이 수정하거나 삭제하고 있는 Row는 읽기, 수정, 삭제가 전부 불가능하다.

  • Shared Lock을 사용하는 쿼리끼리는 같은 Row에 접근이 가능하다. 반면, Exclude Lock이 걸린 Row는 다른 어떠한 쿼리도 접근이 불가능하다.

Record Lock

Record Lock은 Row가 아니라 DB의 index record에 걸리는 Lock이다. 여기도 row-level lock과 마찬가지로 Shared Lock과 Exclusive Lock이 있다.

Record Lock의 예시를 들어보자. c1이라는 column을 가지는 테이블 t가 있다고 하자. 이 때 한 트랜잭션에서 밑과 같은 쿼리를 실행했다. 그러면 t.c1의 값이 10인 index에 Exclusive Lock이 걸린다.

(Transaction A)
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;

이 때, 다른 트랜잭션에서 밑의 쿼리를 실행하려고 하면, t2.c1 = 10인 index record에 Exclusive Lock을 걸려고 시도한다.

하지만 해당 index record에는 이미 Transaction A가 이미 Exclusive Lock을 건 상태이다. 따라서 Transaction B는 Transaction A가 commit되거나 rollback이 되기 전까지, t.c1 = 10인 row를 삭제할 수 없다. 이는 DELETE 뿐만 아니라 INSERT나 UDPATE 쿼리도 마찬가지다.

(Transaction B)
DELETE FROM t WHERE c1 = 10;

Gap Lock

Gap Lock은 DB index record의 gap에 걸리는 Lock이다. 여기서 gap이란 index 중 DB에 실제 record가 없는 부분이다.

예를 들어 설명해보자.

id column만 있는 테이블이 있고, id column에 index가 걸려있다고 하자. 현재 테이블에는 id = 3인 row와 id = 7인 row가 있다. 그러면 DB와 index는 아래 그림과 같은 상태일 것이다.

그러면 현재 id <= 24 <= 1d <= 68 <= id에 해당하는 부분에는 index record가 없다. 이 부분이 바로 index record의 gap이다.

그리고 Gap Lock은 이러한 gap에 걸리는 Lock이다. 즉, Gap Lock은 해당 gap에 접근하려는 다른 쿼리의 접근을 막는다.

Record Lock이 해당 index를 사용하려는 다른 쿼리의 접근을 막는 것과 동일하다. 둘의 차이점이라고 하면, Record Lock은 이미 존재하는 Row가 변경되지 않도록 보호하는 반면, Gap Lock은 조건에 해당하는 새로운 Row가 추가되는 것을 방지하기 위함이다.

Gap Lock에 대한 예시를 살펴보자. c1이라는 column 하나가 있는 테이블 t가 있다. 여기에는 c1 = 13c1 = 17이라는 두 Row가 있다. 이 상태에서 한 트랜잭션에서 밑과 같은 쿼리를 실행했다.

(Transaction 1)
SELECT c1 FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE;

그러면 t.c1의 값이 10과 20 사이 중 실제 record가 없는 부분인 gap에 Lock이 걸린다.

즉, 10 <= id <= 1214 <= id <= 1618 <= id <= 20에 해당하는 gap에 lock이 걸린다. 이 상태에서 다른 트랜잭션이 t.c1 = 15인 row를 삽입하려고 하면, Gap Lock 때문에 트랜잭션 A가 commit 되거나 rollback 될 때까지 삽입되지 않는다. 

INSERT 뿐만 아니라 UPDATEDELETE 쿼리도 마찬가지다. Gap은 하나의 index 값일 수도, 여러 index 값일, 혹은 아예 아무 값도 없을 수도 있다.

트랜잭션의 특성

  • 트랜잭션은 어떠한 특성을 만족해야할까? Transaction 은 다음의 ACID 라는 4 가지 특성을 만족해야 한다.

    • 원자성(Atomicity)
      • 만약 트랜잭션 중간에 어떠한 문제가 발생한다면 트랜잭션에 해당하는 어떠한 작업 내용도 수행되어서는 안되며 아무런 문제가 발생되지 않았을 경우에만 모든 작업이 수행되어야 한다.
    • 일관성(Consistency)
      • 트랜잭션이 완료된 다음의 상태에서도 트랜잭션이 일어나기 전의 상황과 동일하게 데이터의 일관성을 보장해야 한다.
    • 고립성(Isolation)
      • 각각의 트랜잭션은 서로 간섭없이 독립적으로 수행되어야 한다.
    • 지속성(Durability)
      • 트랜잭션이 정상적으로 종료된 다음에는 영구적으로 데이터베이스에 작업의 결과가 저장되어야 한다.
  • Isolation Level

    • ACID의 원칙을 너무 타이트하게 지키면 동시성에 대한 퍼포먼스가 너무 떨어짐. Isolation Level 별로 차등을 두어 동시성에 대한 이점을 가질 수 있게 함. 하지만, 문제가 발생할 가능성이 커짐.

    • 동시에 여러 트랜잭션이 처리될 때

      • 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것.
    • READ UNCOMMITTED

      • 각 트랜잭션에서의 변경 내용이 COMMIT이나 ROLLBACK 여부에 상관 없이 다른 트랜잭션에서 값을 읽을 수 있다.
      • 정합성에 문제가 많은 격리 수준이기 때문에 사용하지 않는 것을 권장한다.
      • 아래의 그림과 같이 Commit이 되지 않는 상태지만 Update된 값을 다른 트랜잭션에서 읽을 수 있다.
      • DIRTY READ현상 발생
        • 트랜잭션이 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있게 되는 현상
    • READ COMMITTED

      • RDB에서 대부분 기본적으로 사용되고 있는 격리 수준이다.
      • Dirty Read와 같은 현상은 발생하지 않는다.
      • 실제 테이블 값을 가져오는 것이 아니라 Undo 영역에 백업된 레코드에서 값을 가져온다.

      • 트랜잭션-1Commit한 이후 아직 끝나지 않는 트랜잭션-2가 다시 테이블 값을 읽으면 값이 변경됨을 알 수 있다.
      • 하나의 트랜잭션내에서 똑같은 SELECT 쿼리를 실행했을 때는 항상 같은 결과를 가져와야 하는 REPEATABLE READ의 정합성에 어긋난다.
      • Non-repeatable read
      • 이러한 문제는 주로 입금, 출금 처리가 진행되는 금전적인 처리에서 주로 발생한다.
        • 데이터의 정합성은 깨지고, 버그는 찾기 어려워 진다.
    • REPEATABLE READ

      • MySQL에서는 트랜잭션마다 트랜잭션 ID를 부여하여 트랜잭션 ID보다 작은 트랜잭션 번호에서 변경한 것만 읽게 된다.
      • 업데이트 삭제는 불허하지만, 삽입은 허용 ⇒ phantom read 발생
      • Undo 공간에 백업해두고 실제 레코드 값을 변경한다.
        • 백업된 데이터는 불필요하다고 판단하는 시점에 주기적으로 삭제한다.
        • Undo에 백업된 레코드가 많아지면 MySQL 서버의 처리 성능이 떨어질 수 있다.
      • 이러한 변경방식은 MVCC(Multi Version Concurrency Control)라고 부른다.

      • PHANTOM READ 발생
        • 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상
        • 이를 방지하기 위해서는 쓰기 잠금을 걸어야 한다.
    • SERIALIZABLE

      • 가장 단순한 격리 수준이지만 가장 엄격한 격리 수준
      • 성능 측면에서는 동시 처리성능이 가장 낮다.
      • SERIALIZABLE에서는 PHANTOM READ가 발생하지 않는다.하지만.. 데이터베이스에서 거의 사용되지 않는다.
  • 트랜잭션을 사용할 때 주의할 점

    • 트랜잭션은 꼭 필요한 최소의 코드에만 적용하는 것이 좋다. 즉 트랜잭션의 범위를 최소화하라는 의미다. 일반적으로 데이터베이스 커넥션은 개수가 제한적이다. 그런데 각 단위 프로그램이 커넥션을 소유하는 시간이 길어진다면 사용 가능한 여유 커넥션의 개수는 줄어들게 된다. 그러다 어느 순간에는 각 단위 프로그램에서 커넥션을 가져가기 위해 기다려야 하는 상황이 발생할 수도 있는 것이다.
  • 교착상태
    복수의 트랜잭션을 사용하다보면 교착상태가 일어날수 있다. 교착상태란 두 개 이상의 트랜잭션이 특정 자원(테이블 또는 행)의 잠금(Lock)을 획득한 채 다른 트랜잭션이 소유하고 있는 잠금을 요구하면 아무리 기다려도 상황이 바뀌지 않는 상태가 되는데, 이를 교착상태라고 한다.

  • 교착상태의 예(MySQL)

    MySQL MVCC에 따른 특성 때문에 트랜잭션에서 갱신 연산(Insert, Update, Delete)를 실행하면 잠금을 획득한다. (기본은 행에 대한 잠금)

    트랜잭션 1이 테이블 B의 첫번째 행의 잠금을 얻고 트랜잭션 2도 테이블 A의 첫번째 행의 잠금을 얻었다고 하자.

    트랜잭션을 commit 하지 않은채 서로의 첫번째 행에 대한 잠금을 요청하면

    Deadlock 이 발생한다. 일반적인 DBMS는 교착상태를 독자적으로 검출해 보고한다.

    mysql은 deadlock 발생시 트랜잭션을 검사하여 변경이 적은 트랜잭션을 roll back 시킨다.

Transaction 1> create table B (i1 int not null primary key) engine = innodb;
Transaction 2> create table A (i1 int not null primary key) engine = innodb;

Transaction 1> start transaction; insert into B values(1);
Transaction 2> start transaction; insert into A values(1);

Transaction 1> insert into A values(1);
Transaction 2> insert into B values(1);

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  • 교착 상태의 빈도를 낮추는 방법
    • 트랜잭션을 자주 커밋한다.
    • 정해진 순서로 테이블에 접근한다. 위에서 트랜잭션 1 이 테이블 B -> A 의 순으로 접근했고, 트랜잭션 2 는 테이블 A -> B의 순으로 접근했다. 트랜잭션들이 동일한 테이블 순으로 접근하게 한다.
    • 읽기 잠금 획득 (SELECT ~ FOR UPDATE)의 사용을 피한다.
    • 한 테이블의 복수 행을 복수의 연결에서 순서 없이 갱신하면 교착상태가 발생하기 쉽다, 이 경우에는 테이블 단위의 잠금을 획득해 갱신을 직렬화 하면 동시성을 떨어지지만 교착상태를 회피할 수 있다.
profile
글 쓰는 개발자

0개의 댓글