3 minute read

트랜잭션은 데이터의 정합성을 보장하기 위한 기능이다.

트랜잭션의 ACID

  • 원자성(Atomocity)
    • 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 성공하거나 실패해야 한다.
      즉, 트랜잭션 안에 있는 일련의 작업들은 하나의 원자처럼 작동해야 한다.
  • 일관성(Consistency)
    • 모든 트랜잭션은 일관성있는 데이터베이스 상태를 유지해야 한다.
  • 격리성(Isolation)
    • 실행되는 트랜잭션들이 서로에게 영형을 미치지 않도록 격리한다.
    • 예를 들자면 동시에 같은 데이터를 수정하지 못하도록 각 트랜잭션을 격리한다.
    • 격리성은 동시성과 관련이 있기 때문에, DB 종류에 맞는 격리 수준을 선택해 격리 수준 정책을 비즈니스 상황에 맞게 변경할 수 있다.
  • 지속성(Durability)
    • 시스템에 문제가 발생하더라고 성공적으로 끝난 트랜잭션이라면 로드 등을 사용해서 성공한 트랜잭션 내용을 복수해서 반영해야 한다.

트랜잭션 격리 수준

위에서 트랜잭션의 격리성에 대해 알아봤는데, 트랜잭션의 격리성은 동시성과 관련이 있기 때문에, 엄격한 격리 수준을 적용하면 성능 저하가 발생할 수 있기에 각 격리 수준에 대한 특성을 파악해서 비즈니스 상황에 맞게 절절한 격리 수준을 적용해야 한다.

트랜잭션 격리 수준의 종류

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

    1 -> 4로 갈수록 격리 수준이 높아진다.

READ UNCOMMITTED(커밋되지 않은 읽기)

  • ‘커밋되지 않는 읽기’는 Dirty Read라고도 하는데, 예를 통해 바로 알아보자.
  • 우선 격리 수준이 필요한 이유는 한 자원에 여러 개의 트랜잭션이 동시에 접근하는 경우에 대한 처리 방식을 다르게 가져가기 위한 것이다.

예를 들어 A와 B 사용자가 User 테이블에 동시에 접근하고 있다고 가정한다.

  1. A 사용자는 트랜잭션을 시작하고 ‘m0o0o0o’라는 아이디를 갖는 새로운 데이터를 생성한다. 그렇지만 아직 커밋을 하지 않았다.
  2. B 사용자 역시 트랜잭션을 시작하고 A 사용자가 커밋하기 전에 ‘m0o0o0o’라는 아이디를 갖는 user의 데이터를 조회한다.
  3. A 사용자는 필드 값에 오류로 인해 insert 했던 데이터를 롤백한다.

위와 같은 예제에서 두 가지 상황을 가정해보자.

  • 위의 2번 과정에서 B 사용자는 ‘m0o0o0o’ 아이디를 갖는 데이터를 조회했다.
    1번 과정에서 A 사용자는 아직 커밋을 진행하지 않아서 해당 트랜잭션이 끝나지 않은 상태이지만, 현재의 격리 수준 ‘READ UNCOMMITTED’는 이름 그대로 커밋되지 않은 데이터를 읽어올 수 있기 때문에 B 사용자는 성공적으로 데이터 조회에 성공하게 된다.
  • 그런데, B 사용자의 데이터 조회 이후 A 사용자가 커밋 대신 롤백을 한다면 여기서 문제가 발생할 수 있다.
  • 바로 2번 과정에서 B 사용자는 롤백된 데이터를 조회했기 때문에 실질적으로 해당 데이터는 db 상에 존재하지 않기 때문에 비즈니스 로직에 따라 심각한 오류가 발생할 수 있기 때문이다.

위와 같이 트랜잭션에서 처리한 작업이 아직 커밋되지 않았음에도 다른 트랜잭션에서 읽을 수 있게 되는 경우를 ‘Dirty Reead’라고도 한다.


READ COMMITTED(커밋된 읽기)

  • READ COMMITTED는 이름에서 유추할 수 있듯 Dirty Read가 발생하지 않는다.
  • 즉, 커밋이 되지 않은 데이터는 읽을 수 없다는 뜻이다.
  • 예를 통해 ‘커밋된 읽기’에 대해 알아보자.

User Table - 유저의 정보가 저장된 테이블

user_id user_name
1 m0o0o0o
  1. 사용자 A가 다음의 쿼리문을 실행했지만, 아직 커밋하지 않는 상황이다
    UPDATE user SET user_name='change_m0o0o0o' WHERE user_id=1;
    
  2. 사용자 B가 다음과 같은 query 문을 실행하고, 결과값을 받았다.
     SELECT * from user WHERE user_id=1;
    

실행결과

user_id user_name
1 m0o0o0o
  1. 사용자 A가 자신의 트랜잭션을 커밋한다.
  2. 사용자 B가 2번 과정에서 실행했던 쿼리문을 다시 실행한다.

실행결과

user_id user_name
1 change_m0o0o0o

위의 예시를 보면 READ COMMITTED는 커밋된 데이터만 가져오는 걸 알 수 있다. 그럼 2번 과정에서 사용자 B는 변경 이전에 데이터를 조회했다. 해당 데이터는 어디서 왔는 지 알아보자.

UNDO 영역

  • UNDO 영역은 UPDATE나 DELETE와 같은 데이터를 변경하는 과정에서 변경 이전의 데이터를 보관해두는 곳이다.
  • 언두 영억의 필요성과 목적은 2가지가 있다.
    1. 트랜잭션의 롤백 대비용
    2. 트랜잭션 격리 수준을 유지하면서 높은 동시성을 제공

READ COMMITTED 격리 수준에서도 NON-REPEATABLE READ라는 부정합 문제가 발생한다. 예를 들자면

  1. A 사용자는 트랜잭션을 열고 데이터를 하나 조회했지만, 조회 결과가 없었고 아직 트랜잭션을 닫지 않았다.
  2. B 사용자가 A 사용자가 조회한 데이터를 생성하고 커밋했다.
  3. A 사용자는 1번에서 조회한 데이터를 다시 조회했는데, 이번에는 조회에 성공했고, 커밋한다.

    위와 같은 경우는 “하나의 트랜잭션 안에서 같은 SELECT문은 항상 같은 결과를 보장해야 한다는 REPEATABLE READ 정합성에 어긋난다”는 규칙을 어기고 있다.

이러한 부정합은 비즈니스 상황에(금융) 따라 문제가 될 수 있기에 보다 높은 수준의 격리 수준을 고려해야 한다.

REPEATABLE READ

  • REPEATABLE READ는 READ COMMITTED 수준에서 발생하는 “NON-REPEATABLE READ” 부정합이 발생하지 않는다.

그 이유는 다음과 같다.

MVCC(Multi Version Concurrency Control)
트랜잭션이 롤백될 가능성에 대비해 변경되기 전 레코드를 언두 영역에 백업해두고 실제 레코드 값을 변경한다. 이런 방식은 MVCC라 한다. REPEATABLE READ는 MVCC를 위해 언두 영역에 데이터를 통해 동일한 트랜잭션 내에서 동일한 결과를 보여줄 수 있다.

하지만 REPEATABLE READ에서도 “PHANTOM READ”가 발생할 수 있다. (InnoDB에서는 발생하지 않는다고 합니다.)

SERIALIZABLE

  • SERIALIZABLE는 가장 단순하면서 가장 엄격한 격리 수준이다.
  • 또한 그 전의 격리 수준들을 사용해도 처리 성능에 큰 영향을 미치지 않지만, SERIALIZABLE 격리 수준은 다른 격리 수준보다 처리 성능이 현저히 떨어진다.
  • SERIALIZABLE 격리 수준이 설정되면 읽기 작업도 공유 잠금을 획득해야 한다. 동시에 다른 트랜잭션은 해당 레코드를 읽거나 쓸 수 없다.
  • 즉, 한 트랜잭션에서 읽거나 쓰는 모든 레코드를 다른 트랜잭션에서는 절대 접근할 수 없다.