DataBase/DB

[DataBase] 트랜잭션 공부하기

민돌v 2024. 10. 26. 00:18

안녕하세요 오늘은 트랜잭션에 대해서 공부해보고자 합니다.
공부하기 전에 목차를 정해야겠죠
 
[목차]

  1. 트랜잭션이란
    • 트랜잭션이 필요한 이유
  2. 트랜잭션의 특징
  3. 트랜잭션의 격리 수준
    • 트랜잭션의 격리 수준이 있는 이유
  4. 트랜잭션의 동작 과정 (Query)

시작!


1. 트랜잭션이란 (Transaction)

✔️ 트랜잭션의 사전적(개인적) 정의

  • 트랜잭션이란, 작업의 완전성을 보장해주기 위한 시스템입니다.
  • 어떤 작업을 처리하기위한 작은 작업단위들이 세팅되었을 때, 이 작업 세트들이 모두 논리적으로 묶여 하나의 작업 단위로 구성되어
    모든 논리적인 작업이 성공되었을 때 -> 작업이 성공되었음을 관리하기 위한 시스템으로 이해됩니다.
  • 트랜잭션은 작업의 정합성을 지키기 위한 시스템입니다.

 

✔️ 트랜잭션이 필요한 이유

1개의 작업을 완료하기 위해서는 하위 작업들의 연속된 성공이 보장되어야 합니다.
예를 들어 "9시까지 출근한다" 라는 작업을 성공하기 위해서는 아래와 같은 작업들이 모두 성공해야합니다.

  1. 6:30 에 기상한다.
  2. 7:00 까지 씻는다.
  3. 7:05 에 버스를 탄다.
  4. 7:30 에 지하철을 탄다.
  5. 8:50 에 사무실에 도착한다.
  6. 9:00 전에 출근도장을 찍는다.

→ 이 모든 작업이 순차적으로 성공해야만, 작업이 완료되었다라고 보았을 때 1가지라도 실패하면 작업은 실패햐애합니다.
예를들어, "9시 까지 출근한다" 라는 작업이후 "9:10 에 중대한 회의를 시작한다." 라는 작업이 실행되어야 한다면 선행 작업이 실패했을 때 후행 작업이 실행되면 안됩니다.

  1. 6:30 에 기상한다.
  2. 7:00 까지 씻는다.
  3. 7:05 에 버스를 탄다.  → 버스를 놓쳤다. (이후의 하위 작업은 의미가 없어짐)
  4. 7:30 에 지하철을 탄다.
  5. 8:50 에 사무실에 도착한다.
  6. 9:00 전에 출근도장을 찍는다.

선행작업(9:00) 출근을 하지 못하였지만,
선행작업(정상출근)이 실패한 것을 모르고 후행 작업 (9:10 회의) 가 저 없이 진행된다면,, 대참사가 일어납니다 !! (아마도요)
 선행작업이 실패했다면, 다시 과거로 돌아가 (Roll Back) 작업을 성공시킨 후 후행 작업을 실행하여야 합니다.
 


2. 트랜잭션의 특징

이제 트랜잭션이 무엇이고, 왜 필요한지 생각해보았으니 "트랜잭션이란 시스템에서 가져야하는 특징" 에 대해 정리하겠습니다.
트랜잭션이란 개념은 나온지 오래되었고 가져야하는 특징에 대해 이미 정의가 되어있습니다.
흔히 ACID 라고 합니다.

  • 원자성, 일관성, 고립성, 영속성

 

1. 원자성 Atomicity

트랜잭션의 작업단위가 가장 작은 단위여야 한다. 
  • 즉 논리적으로 구성된 작업셋 → 트랜잭션 → 가장 작업의 단위가 되어야하며
  • 이 의미는, 트랜잭션으로 설정된 모든 작업이 성공되어야 DB 에 반영(Commit) 되어야 하며, 작업 셋 중 1가지로 실패하게 되면 DB (데이터 변경)에 반영되어서는 안됩니다.
  • 흔히 "트랜잭션에 포함된 작업은 전부 수행되거나 전부 수행되지 않아야한다" 라고 표현하기도 합니다.

 
📌 "트랜잭션의 작업단위가 가장 작은 작업의 단위가 되어야 한다." 는 다른 의미로
      "트랜잭션의 범위를 최소화 하라" 는 말과 같게 느껴집니다.
 → 트랜잭션 또한 자원입니다.
트랜잭션을 유지하기 위해서는 DB 커넥션을 유지해야 하며, I/O 자원도 잡아 먹습니다.
또한, 작업의 편의성을 위해 트랜잭션을 길게 가져간다면, 실패했을 경우 Rollback 에 대한 자원 소모가 커집니다. 


2. 일관성 Constency

트랜잭션을 수행하기 전이나 후나 데이터베이스는 항상 일관된 상태를 유지해야한다.
  • "트랜잭션의 수행 전, 후에 데이터 모델의 모든 제약 조건(기본키, 외래 키, 도메인, 도메인 제약조건 등)을 만족해야한다." 입니다.
    • 예를 들자면
      1. A 에서 B로 돈을 이체했을 때 A와 B 돈의 총합은 같아야 한다.
      2. A 의 외래 키를 참조하는 B 테이블이 있을 때, A의 외래 키가 변경되면 B의 참조 값도 변경되어야 한다.
    • 즉, 데이터가 일관되게 보관되고 저장되어야한다. 정도로 이해됩니다.

3. 격리성 (고립성) Isolation

트랜잭션 수행 시, 다른 트랜잭션이 작업에 영향을 주어서는 안된다.
  • 데이터베이스는 기본적으로 클라이언트(호출부)가 같은 데이터를 공유(공유자원)하고 그 자원을 효율적으로 관리하는 것을 목표로 합니다.
  • 따라서 같은 자원을 함께 사용할 때가 존재합니다. → 즉 여러 트랜잭션이 동시에 수행되어야 합니다.
  • 이때 트랜잭션은 상호 간의 존재를 모르고 독립적으로 수행되어야 자원의 일관성(or 정합성) 을 유지할 수 있습니다.
  • 트랜잭션간의 간섭이 일어나지 않도록 하기위해 공유자원에 접근하는 트랜잭션에 대한 제어가 필요합니다. (격리 필요)

4. 영속성 (지속성) Durability

트랜잭션의 작업이 성공하여 일어진 데이터의 변경은 영구히 반영되어야한다.
  • Commit 이 완료된 트랜잭션(성공한 작업)에 의한 데이터 변경은 그 즉시 데이터베이스의 데이터에 반영되어야 합니다.
  • 다른 트랜잭션의 간섭에 영향을 받지않고, 반영된 데이터가 그 다음에 실행되는 트랜잭션에 정상적으로 변경된 값이 조회되어야만 합니다.
  • 그렇지않으면 일관성이 깨지니까요!

 


3. 트랜잭션의 격리 수준

트랜잭션의 격리 수준이란, 트랜잭션이 가져야하는 4가지 특징(ACID) 중 Isolation(고립성) 을 지키기 위해 시스템으로 고안해 낸 몆가지 방법들 입니다.
목차에는 [트랜잭션의 격리 수준이 있는 이유] 라고 적었지만
DBMS 에서 공유자원에 접근하는 트랜잭션간의 동시성을 제어하기 위한 조치 방법이 "트랜잭션 격리수준" 입니다.

(동시성에 대하여 공부하고 싶다면 해당 게시물 참고 → https://thalals.tistory.com/485)

공유자원에 동시에 접근하는 프로세스들에 대하여 접근을 얼마나 고립시킬것인지, 얼마나 허용시킬지에 따라서 성능과 안전성이 반비례하여 달라집니다.


격리수준은 크게 아래의 4가지로 나뉩니다.
위로 갈수록 격리수준이 높고 성능이 동시성 처리 성능이 낮으며, 아래로 내려갈수록 격리수준이 낮고 동시성 처리 성능이 뛰어납니다.

  1. SERIALIZABLE
  2. REPEATABLE READ → (MySQL default)
  3. READ COMMITTED → (Oracle default)
  4. READ UNCOMMITTED

(👏 다만, Real MySQL 8.0에 의하면 SERIALIZABLE 수준이 아니면 크게 성능의 개선이나 저하는 발생하지 않는다고 합니다.)
이제 각 격리수준이 얼마나 격리하는 것인지, 또한 각 수준별로 어떠한 문제점이 발생하길래 4가지나 시스템적으로 나누어 놓았는지 알아보도록 하겠씁니다.


1. SERIALIZABLE 

가장 단순하면서도 엄격한 격리수준입니다.
  • 트랜잭션이 Update 시 뿐만 아니라, Select 시에도 레코드 락을 걸어 읽기 작업 시에도 공유 잠금(읽기 잠금)을 획득해야 데이터에 접근이 가능합니다.
  • 즉, 한 트랜잭션에서 Read/Write 하는 데이터에는 어떠한 트랜잭션도 절대 접근할 수 없습니다.
  • 가장 엄격한 만큼 트랜잭션 격리 수준에서 발생하는 3가지 부정합 문제 중 어떠한 것도 일어나지 않지만, 동시성 제어 처리 성능이 떨어집니다.

 
🤔 Real MySQL 8.0 에 의하면 아래와 같은 말이 나옵니다,, phantom read 현상에 대해서는 아래 REPEATABLE READ 격리 수준에서 살펴보고자 합니다.

"InnoDB 스토리지 엔진 에서는 갭락과 넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도 이미 PHANTOM READ (유령 읽기) 부정합 문제가 발생하지 않기 때문에 굳이 SERIALIZABLE을 사용할 필요성은 없어 보인다."

"다만, 엄밀하게 말해서 "SELECT ... FOR UPDATE" 또는 "SELECT ... FOR SHARE" 쿼리의 경우 REPEATABLE READ 격리 수준에서 PHANTOM READ 현상이 발생할 수 있다."

"하지만 레코드의 변경 이력(언두 로그)에 잠금을 걸 수는 없기 때문에, 이러한 잠그을 동반한 SELECT 쿼리는 예외적인 상황으로 볼 수 있다."

2. REPEATABLE READ

반복된 읽기? 이게 무엇일까 - 반복해서 읽어도 데이터 값이 일관성을 유지한다는게 아닐까?
(REPEATABLE READ 격리 수준은 MySQL의 InnoDB 스토리지 엔진에서 기본을 사용되는 격리 수준입니다.) 
  • MySQL InnoDB 스토리지 엔진에서는 트랜잭션이 Rollback 될 가능성에 대비해 데이터가 트랜잭션에 의해 변경되기 전의 값을
    "언두(Undo)" 공간에 백업해두고 실제 레코드 값을 변경합니다.
  • REAPATABLE READ 격리 수준에서는 트랜잭션마다 번호를 부여하고, 언두 공간에 데이터를 백업할 떄 값을 변경한 트랜잭션 번호를 함께 저장합니다.
  • 즉, 트랜잭션의 버전마다 변경되는 데이터를 관리하는 MVCC(Multi Version Concurrency Control) 방식을 사용합니다.
  • REPEATABLE READ 수준에서는 언두 영역에 백업된 이전 데이터를 이용해 동일 트랜잭션 내에서는 반복해서 데이터를 조회하더라도 동일한 결과값을 보여줄 수 있도록 보장합니다.

 
→ InnoDB 스토리지 엔진에서는 불필요하다고 판단하는 시점에 주기적으로 언두영역의 백업된 데이터를 삭제합니다.
→ 다만, 장시간 트랜잭션을 종료하지 않으면 언두 영역이 백업된 데이터로 무한히 커져 백업 레코드가 많아지면, DB 서버의 처리 성능이 저하될 수 있습니다.


✔️ REPEATABLE READ 격리 수준에서 트랜잭션이 동작하는 과정

사용자 A 와 B는 같은 테이블을 바라봅니다.

  1. 사용자 B 의 트랜잭션이 시작하여 10번이라는 번호를 부여 받습니다.
  2. 이후, 사용자 A의 트랜잭션이 시작하여 12번이라는 번호를 부여받습니다.
    1. 트랜잭션 12번은 테이블의 특정값을 변경합니다. (emp=500000 레코드, Lara → Toto)
    2. 이때 REPEATABLE READ 격리 수준에서는 트랜잭션 12번으로 변경되기 전의 데이터를 언두 영역에 저장합니다. (이전 트랜잭션 번호와 함께)
    3. 트랜잭션 12번이 Commit 되고 실제 데이터에 반영됩니다.
  3. 사용자 B의 트랜잭션(10번) 이 다시 emp=5000 레코드의 값을 조회합니다.
  4. 실제 데이터는 사용자 A에 의해 변경되었지만, 사용자 B의 트랜잭션(10번)은 부여받은 자신의 트랜잭션보다 작은 트랜잭션 번호에서 변경한 이력만 바라봅니다.

👏 REPEATABLE READ 격리 수준에서는 이러한 MVCC 방식으로 동일 트랜잭션 내에서는 동일한 결과값이 조회될 수 있도록 보장합니다.


✔️ REPEATABLE READ 격리 수준에서 발생하는 부정합 문제 (PHANTOM READ)

REPEATABLE READ 격리 수준은 SERIALIZABLE 만큼 엄격한 동시성제어가 아니기 때문에 부정합 문제가 발생합니다.
"PHANTOM READ (유령 읽기)"

  • 유령읽기 : 이름 그대로 존재하지 않는 데이터를 읽는다는 의미입니다.
  • 아래의 그림과 같이, 사용자 B의 트랜잭션이 유지되고 있을 때 다른 트랜잭션에 데이터를 Insert 한 상황입니다.
  • 이후 트랜잭션 10 번에서 "SELECT .. FOR UPDATE" 와 같이 락(Lock)을 걸어 조회할 때 문제가 생깁니다.
  • 언두 영역에는 락을 걸 수 없기 때문에, 변경이 일어난 실제 테이블(당연히 트랜잭션 10번 보다 최신화 상태)을 바라보게 되고
    쿼리 결과가 달라지게 됩니다.
  • 이렇게 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다, 안보였다 하는 현상을 PHANTOM READ 라고 합니다.
정리 : 언두영역에는 Lock 을 걸 수 없기 때문에, Lock 을 거는 쿼리를 수행할 때 실제 레코드를 보게 되고 이때 버전이 맞지 않으므로 데이터 부정합이 발생한다.

3. READ COMMITTED

커밋된 데이터만 읽겠다.
오라클 DBMS 에서 기본으로 사용되는 격리 수준입니다.
  • READ COMMITED 격리수준은, 어떤 트랜잭션에서 데이터를 변경했더라도 Commit이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있습니다.
  • READ COMMITED 격리수준에서도 마찬가지로 데이터를 변경할 때 언두(Undo)영역에 변경 이전의 데이터를 저장하여 다른 트랜잭션에 의해 변경된 데이터가 commit 되기 전이라면 조회하지 이전 데이터를 조회합니다.
  • 다만, REPEATABLE READ 와 다르게 Commit 에 완료되면 Commit 된 데이터를 조회합니다.

✔️ REPEATABLE READ 격리 수준에서 발생하는 부정합 문제 (NON-REPEATABLE READ)

  • 아래의 그림과 같이, 실행되고 있는 트랜잭션에서 연속해서 데이터를 조회하는 상황이 있습니다.
  • 특정 시점에 다른 트랜잭션에 의해 데이터가 변경 후 commit 이 안료되면, 작업 중인 트랜잭션에 데이터를 조회하는 결과값이 달라집니다.
  • 즉, 항상 같은 결과를 가져와야 한다는 "REPEATABLE READ" 정합성에 위배되는 문제가 발생합니다.
  • 이를 NON-REPEATABLE READ 부정합 문제라 합니다.

 

4. READ UNCOMMITED

가장 낮은 수준의 격리 수준입니다.
3가지 부정합 문제가 발생하며, MySQL 에서는 최소한 READ COMMITED 이상의 격리 수준을 사용할 것을 권장합니다.
  • READ UNCOMMITED 격리 수준에서는 아래 그림과 같이 Commit 이나 Rollback 여부에 상관없이 모든 트랜잭션의 변경 내용이 조회됩니다.
  • 이렇게 되면, 다른 트랜잭션에 의해 변경되 값을 조회한 트랜잭션이 존재하고 값을 변경한 트랜잭션이 Rollback 된다면 문제가.. 생기겠죠?
  • 이처럼 어떤 트랜잭션에서 처리한 작업이 완료되지 않았음에도 다른 트랜잭션에서 볼 수 있는 현상을 "더티 리드(Dirty Read)"라고 합니다.

📌 트랜잭션 격리 수준에 의한 부정합 문제 정리 표

 


4. 트랜잭션의 동작 과정

MySQL 에서 트래잭션을 실행할 때 실질적으로 날라가는 쿼리를 간단하게 알아보았습니다.

  1. 자동커밋 해제
    • 기본적으로 데이터베이스는 자동커밋으로 설정되어 있습니다.
    • 즉, 쿼리가 실행될 때 데이터베이스에 즉각적으로 반영됩니다.
    • 트랜잭션은 즉각적으로 반영되지 않도록 하는 것이기 때문에, 자동 커밋을 해제하고 수동 커밋 모드로 변경하는 것이 트랜잭션의 시작입니다.
  2. 쿼리 실행
    1. 수동커밋 모드로 변경 후 쿼리를 실행합니다.
    2. 이때 실행한 쿼리는 해당 세션 내에서만 동작하는 것이기 때문에 다른 트랜잭션에서는 해당 트랜잭션의 쿼리 실행 결과에 대해서 조회할 수 없습니다. (커밋 전)
  3. 커밋, 롤백
    1. 만약 문제없이 트랜잭션이 마무리 되었다면 실행 쿼리들에 대한 커밋
    2. 문제가 발생했다면 롤백
set autocommit false; // 수동커밋 모드로 설정
# set autocommit true; //자동 커밋 모드로 설정
 
UPDATE user SET name='lora' WHERE user_id=1;

#성공 시
commit;
#실패 시
rollback;

 
Spring 에서 트랜잭션을 실행할 때도 하이버네이트 내부적으로도 동일하게 DBCP에서 커넥션을 가져올 때 setAutoCommit() 메소를 호출하여
autocommit = false 하는 쿼리가 날아갑니다. (성능 최적화 가능 지점)

자세한 링크 첨부하고 마무리하겠습니당
https://pkgonan.github.io/2019/01/hibrnate-autocommit-tuning

 
 

끝! 아 졸려!


참고