
- 트랜잭션은 내결함성을 갖추기 위해 전체가 성공(
Commit)하거나 전체가 실패(Rollback)해야 하는 단순한 매커니즘으로 채택되어옴
- 이는 어느 정도의 잠재적인 오류 시나리오와 동시성 문제를 무시할 수 있도록 하여 우리 애플리케이션의 프로그래밍 모델을 단순화 해줌
- 그럼에도 트랜잭션이 꼭 필요한 것이 아니고, 상황에 따라 애플리케이션 로직만으로 안전성을 보장할 수 있음
애매모호한 트랜잭션의 개념
- 높은 성능과 고가용성을 위해 트랜잭션을 포기해야 한다!
- 트랜잭션의 보장은
중요한 데이터가 있는 중요한 애플리케이션에 필수적이다!
현재 트랜잭션을 둘러싸고 두 가지 주장이 대립하고 있다.
- 현대의 거의 모든 RDB는 트랜잭션을 지원함
- 하지만 2000년대 후반부터 복제와 파티셔닝등의 기능을 제공하며 NoSQL이 인기를 얻음
- NoSQL들이
CAP이론에 기반하여 자연스럽게 트랜잭션을 지원이 어려워짐
ACID의 의미
요즘 ACID를 준수한다는 말이 어떤 의미인지 분명이 파악하기 어려운 상황이되었다.
ACID의 구성
Atomicity - 원자성
Consistency - 일관성
Isolation - 고립성
Durability - 지속성
- 데이터베이스에서 내결함성 매커니즘을 나타내는 용어를 표현하기 위해
ACID를 만듬
- 하지만 실제 데이터베이스들이 준수하는
ACID의 디테일은 조금씩 다름
ACID를 따르지 않는 시스템은 BASE라 불린다.
Basically Available - 대체로 가용함
Soft state - 유연한 상태
Eventual Consistency - 최종적 일관성
원자성
일반적으로 원자성은 멀티스레드 환경에서 로직이 절반만 완료되는 상황이 발생하지 않는 성질을 말한다.
하지만 트랜잭션에서의 원자성은 이와 관련이 없다.
오히려 Abort 능력(abortability)이라 부르는게 더 적합하다.
- 트랜잭션에서 묶여 있는 로직이 하나의 fault 때문에 커밋될 수 없다면 abort 됨
- DB는 기존의 쓰기 요청을 무시함
- 트랜잭션이 없다면 재시도 요청에 중복 쓰기가 발생할 수 있지만, 트랜잭션 덕분에 단순한 방법으로 재시도 가능함
일관성
일관성은 여러 의미로 쓰임
BASE의 eventual consistency(최종적 일관성)
최종적 일관성 - 일시적으로 불일치가 발생할 수 있지만, 결국은 스스로 해소됨을 보장
- 파티셔닝 과정에서 리밸런싱을 위한 일관성 해싱
일관적 해싱 - 파티셔닝된 환경에서 특정 파티션이 핫스팟이 되지 않도록 키를 해싱을 돌려서 파티션의 경계를 정해주고 해당 파티션에 데이터를 저장함
- CAP 정리의 일관성(
선형적의미로 쓰임)
선형적 - 클라이언트가 쓰기를 성공적으로 완료하자마자 그 DB를 읽는 모든 클라이언트가 방금 쓰여진 값을 볼 수 있음을 보장
- ACID에서의 일관성
ACID에서의 일관성
데이터에 대해 불변속성이 항상 참으로 유지되어야 한다.
(ex. age는 0 이상이어야 한다, 송금 과정에서 대변과 차변이 일치해야 한다)
- 일관성은 애플리케이션의 불변속성에 의존한다.
결국 이를 유지하기 위한 책임은 애플리케이션에 있다.
- 원자성, 격리성, 지속성은 데이터베이스의 속성이지만 일관성은 애플리케이션의 속성이다.
- 실제로 ACID라는 약어를 만들기 위해 C가 끼어든 것이지 중요하게 생각하지 않는다고 언급했다.
격리성
실제 트랜잭션이 동시에 실행되더라도 커밋되었을 때의 결과가 트랜잭션이 순차적으로 실행됐을 때의 결과와 같음을 보장하는 것이다.
동일한 레코드에 접근하면 동시성 문제를 만날 수 있다.
ACID에서 격리성은 동시에 실행되는 트랜재션은 서로 격리됨을 의미한다.
지속성
데이터베이스의 목적은 데이터를 잃어버릴 염려가 없는 안전한 저장소를 제공하는 것이다.
지속성은 트랜잭션이 성공적으로 커밋되면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터가 손실되지 않는다는 보장이다.
-
단일 노드의 데이터베이스에서의 지속성은 일반적으로 저장장치(HDD, SDD)에 기록됨을 의미
데이터베이스가 죽었을 때 redo로그를 이용해서 복구하는 방식과 같은 방법으로 지속성을 보장
-
replica가 있는 상황에서 지속성을 유지하려면 모든 노드들이 해당 데이터에 대해 성공적으로 커밋이 되었다는 것을 기다려야한다.
단일 객체 연산과 다중 객체 연산
다중 객체 트랜잭션은 데이텅의 여러 조각이 동기화된 상태로 유지되야 할 때 필요하다.
select count(*) from emails where recipient_id = 2 and unread_flag = true
- 사용자가 읽지 않은 메시지의 개수 쿼리
count(*) 쿼리가 느려서, 읽지 않은 메시지 개수를 별개의 필드로 저장하는 상황
- 메시지를 읽었다고 표시할 때 읽지 않은 메시지 개수도 감소시켜야 함
격리성 오류(더티리드 상황)

- 트랜잭션이 다른 트랜잭션에서 썻던 커밋되지 않은 데이터를 읽음
- 우편함 목록에서 읽지 않은 메시지가 존재함
- 하지만 읽지 않은 메시지 개수는 아직 증가되지 않아서 0으로 반환함
- 격리성을 보장했다면, 새로 추가된 메일과 읽지 않은 메일의 개수가 모두 보이거나 모두 보이지 않음을 보장해준다.
일관성이 깨진 중간 상황을 보는 일이 없게 해준다.
원자성 오류
위 다이어그램에서 insert문이 성공하고 update문에서 실패한 경우 트랜잭션은 원자적으로 commit되거나 abort되어야 한다.
NoSQL에서 다중 객체 연산
NoSQL에서는 다중 객체 연산을 묶어주는 방법이 없는 경우가 많다.
다중 객체 API가 존재할 수는 있지만 우리가 생각하는 트랜잭션을 의미하지 않을 수 있다.(아마 Base를 의미)
단일 객체 쓰기
원자성과 격리성은 단일 객체를 변경하는 경우에도 적용된다.
단일 객체 쓰기 과정에서의 문제
20KB의 Json 문서를 데이터베이스에 저장하는 상황
- 10KB까지 전송한 상황에서 네트워크가 끊긴 경우, 모두 저장되지 않아야 함
- DB에 기존 값을 덮어쓰는 도중 DB가 죽으면, 모두 반영되지 않아야 함
- 문서를 쓰고 있을 때 그 값을 읽어도, 그 값이 조회되지 않아야 함
해결 방법
- 원자성은
undo 로그를 이용해 복구
- 격리성은 락을 이용해 보장
MySQL은 MVCC를 이용해 보장
조금 다른 해결 방법
- 일반적으로 데이터를 변경할 때
read-modify-write의 과정을 거친다.
- 이 과정을 원자적으로 동작할 수 있음을
CAS(compare-and-set)의 과정으로 보장한다.
- 이는 변경하려는 값이 누군가에 의해 동시에 바뀌지 않았을 경우에만 쓰기가 되도록 허용한다.
- 갱신 손실을 방지하기 때문에 좋은 방법이지만, 일반적으로 쓰이는 트랜잭션의 개념은 아니다.
오류 어보트 처리
어보트(Abort)의 취지는 안전하게 재시도를 할 수 있도록 하는 데 있다.
- 트랜잭션이 ACID를 위반할 위험이 있으면, 절반 정도 완료된 상태에 머물게 하는 대신 트랜잭션을 완전히 폐기한다.
- 하지만 모든 데이터베이스가 이러한 철학을 따르는 것은 아니다. 일부 데이터베이스는 현재까지 저장된 내용을 취소하지 않고 오류 복구는 애플리케이션의 책임을 돌린다.
- 오류는 필연적으로 발생하지만 많은 개발자들이 이를 외면하고 낙관적인 상황만을 생각하려 하고, 나아가 어보트된 트랜잭션을 재시도하려 하지 않는다.
- 어보트의 취지는 안전하게 재시도 할 수 있는 데 있다.