01. ACID
유데미 Hussein Nasser님의 데이터베이스 엔지니어링 마스터 강의를 정리한 노트
# ACID 소개
ACID
- 관계형 데이터베이스 시스템(RDB)의 중요한 속성
Agenda
- What is a Transaction?
- Atomicity (원자성)
- 왜 일관성을 보장하는데 원자성이 가장 중요할까?
- Isolation (고립성)
- 트랜잭션이 고립되어있다는 것은 무슨 의미일까?
- 여러 트랜잭션을 동시에 시작하면 이 모든 것들이 내 데이터에 접근하고 변경을 하려하면?
- 그들이 무엇을 볼 수 있는지, 보아서는 안되는 지?
- 이 모든 것들이 일관성에 영향을 끼침
- 일관성 이야기에 앞서서 원자성과 고립성을 먼저 이야기하는 것이 중요하다 생각하여 순서를 배치함
- Consistency (일관성)
- Durability (지속성, 영속성)
- 트랜잭션을 커밋하거나 저장할 때 실제로 데이터가 비휘발성 저장소에 지속적으로 저장되어 내 데이터에 접근할 수 있어야 한다.
- DB가 충돌하거나 전원이 꺼져도 내 데이터에 여전히 접근할 수 있어야 한다.
# 트랜잭션이란?
Transaction
- 하나의 작업 단위
- 하나의 작업 단위로 취급되는 SQL 쿼리들의 모음
- 트랜잭션은 데이터를 변경할 수도, 읽기 전용일 수도 있음
- 트랜잭션은 항상 시작됨
- 만약, 우리가 트랜잭션을 시작하지 않으면 DB 쪽에서 우리 대신 시작함
- 사용자가 정의한 트랜잭션일 수도, 시스템에 의해 암시적으로 정의한 트랜잭션일 수도 있음
데이터는 구조화되어있고, 여러 테이블을 가지고 있기에 하나의 쿼리로 원하는 모든 작업을 수행하기에 어려움
# 트랜잭션의 수명
- Transaction BEGIN
- Transaction COMMIT
- Transaction ROLLBACK
- Transaction unexpected ending = ROLLBACK (ex. crash)
트랜잭션은 항상 여러 쿼리가 포함된 새로운 트랜잭션을 시작하겠다는 것을 DB에 알리는 BEGIN으로 시작
- 작성한 쿼리를 모두 만족하는 트랜잭션이 지속성을 갖는 것은 아님
- 트랜잭션의 수명 동안 내가 진행한 모든 변경사항을 커밋하여 디스크에 영구적으로 저장하라고 알려줘야함
1000개의 쿼리를 실행하고 1000개의 변경을 커밋한다고 하자. 어떻게 DB를 구축할래?
- 모든 변경을 실제로 디스크에 모두 쓰는 경우
- 기다렸다가 메모리에 모은 다음, 커밋할 때 한꺼번에 디스크에 쓰는 경우
첫 번째 방법은 커밋을 빠르게 할 것이고, 두 번째 방법은 커밋을 느리게 할 것이지만 디스크 I/O가 줄어들겠지. 모든 것에는 장단점이 있음
예를 들어, 권장되지는 않지만 50,000개의 쿼리를 실행하다가 문제가 생겼다고 하자. 롤백해야겠지?
- 중간에 진행되었던 변경사항을 커밋하지말고 모든 변경사항을 되돌려
- 위의 1000개 쿼리 문제도 다시 생각해보자. 이 모든 시간동안 쿼리를 실행할 때 실제로 디스크에 영구적으로 저장되어있는지 확인하나요?
- 만약, 그러한 작업을 한다면 모든 작업을 되돌려야 하는데 그것 또한 엄청나게 많은 작업…
- 두 번째 방법처럼 메모리에 모으는 경우라면 실제 디스크에는 아무것도 쓰지 않았기에 롤백이 더 빨라질 것 → 그냥 메모리 비우면 되잖아 ~
- 롤백을 위한 undo, redo 공간이 있으며 모든 변경사항에 대해 작동함
예를 들어, 권장되지는 않지만 20,000개의 쿼리를 실행하다가 시스템이 다운되었다고 하자.
- 롤백해야하는데 데이터베이스 시스템이 다운됐어. 어떻게 할래?
- 데이터베이스가 다시 시작할 때, 이를 롤백해야함
이러한 방법들을 실제 구현하는 것은 MySQL, Postgres, Oracle 등 각각 다르게 구현됨
- 시스템 다운에 최적화 할 지, 커밋에 최적화 할 지 등등
- Postgres는 커밋을 가장 빠르게 처리함
- 트랜잭션 중에 실행된 모든 쿼리 변경사항을 영구적으로 저장하도록 시도하기 때문
- Postgres는 많은 I/O를 수행하지만 커밋은 아주 빠름
- 커밋 자체가 빠르기에 커밋 중에 시스템이 다운되는 상황이 발생할 가능성이 낮아짐
# 트랜잭션의 본질
- 트랜잭션은 데이터를 변경하고 수정하는데 사용됨
- 하지만, 실제로는 읽기 전용 트랜잭션을 가지는 것도 좋음
- DB에게 읽기 전용 트랜잭션이라고 알려주는 것
- 읽기만 하려면 트랜잭션 없이 각 쿼리를 자체 트랜잭션으로 처리하면 안되나?
- Consistency(일관성)을 위해 읽기만 하더라도 트랜잭션이 필요함
예를 들어, 보고서를 생성하고 거래시간을 기준으로 일관된 스냅샷을 얻고자 하는 상황
- 트랜잭션에서 제공하는 것은 작업해야할 이 그룹뿐만 아니라 트랜잭션이 시작한 시간의 스냅샷까지 얻어야함
- 내가 읽는 모든것은 처음 시간을 기준으로 하기를 원함
- 이것이 읽기 전용 트랜잭션의 힘인데, 무엇이든지 읽을 때 무언가가 동시 트랜잭션 때문에 바뀐다고 하더라도 우리는 신경쓸 필요가 없어짐 → Isoloation 파트에서 더 알아보자
예시
- 계좌의 잔액이 음수가 되는 것은 마이너스 통장과 같은 특별한 상황이 아니라면 의미 X
- 즉, 음수인 데이터가 발생하면 일관성이 없는 데이터를 의미
# 원자성 (Atomicity)
- 트랜잭션 내의 모든 쿼리는 성공해야함, 나눌 수 없는 원자처럼 취급함
- 만약, 하나의 쿼리가 실패하면?
- 잔고가 마이너스로 가거나, 중복된 키 항목이 생기거나, 잘못된 SQL 구문이 있거나 등
- 같은 트랜잭션 내에 성공한 쿼리가 100개가 있다고 하더라도 하나의 실패한 트랜잭션으로 인해 즉시 롤백되어야함
- 만약, 트랜잭션 실행 중에 데이터베이스가 다운된다면?
- 커밋 이전에 다운된다면 실제로 트랜잭션을 커밋하지 않았음
- 모든 성공한 쿼리와 트랜잭션은 롤백되어야함
- 그런데, 데이터베이스가 다운되어 롤백을 못하잖아
- → 데이터베이스가 다시 시작되었을 때 데이터베이스는 실패가 있었다고 감지해야함
결국, 데이터베이스마다 실제 구현이 달라서 트레이드오프를 고려하여 선택해야 함
- 어떤 DB는 모든 트랜잭션을 커밋하기도 전에 모든 변경사항을 디스크에 기록함
- 사용자가 커밋할 것이라고 낙관적으로 가정하는 것
- 실제로, 커밋 당시에 어떤 작업도 추가적으로 진행하지 않고 그냥 “이 트랜잭션이 커밋되었다” 정도만 작성함
- 쿼리의 속도는 느리지만 커밋의 속도는 빠름
- 어떤 DB는 디스크에 기록하지 않고 메모리에 모아놨다가 한 방에 flush
- 쿼리의 속도는 빠르지만 커밋의 속도는 느림
- 롤백은 상대적으로 빠름
- 디스크에 기록하지 않았고, 메모리만 비워버리면 되니까