동시성과 싸우기, 트랜잭션 - 데이터 중심 애플리케이션 설계 7장

Broccolism·2022년 3월 7일
2
post-thumbnail

데이터 중심 애플리케이션 설계, 마틴 클레프만 지음. OREILLY.

트랜잭션이란

정의

= 데이터베이스 읽기, 쓰기 작업 N개를 atomic 연산으로 묶은 것.

  • atomic
    • 더이상 쪼갤 수 없음을 의미한다. 예를 들어, 트랜잭션 하나에 쓰기 작업이 3개 있었다면 3개 중 일부만 성공하는 경우는 있을 수 없다. 전부 다 성공하거나 전부 다 실패하거나, 둘 중 하나만 가능하다.

목적

DB에 접속하는 앱이 편-안 해지려고.

  • 즉, DB에 들어오는 요청으로 인해 생길 수 있는 여러가지 문제를 앱이 아닌 DB가 해결해주기 위해 트랜잭션이 생겼다.

트랜잭션이 보장하는 것

트랜잭션이 보장하는 것은 여러가지가 있다. 이를 줄임말로 표현한게 'ACID' 이다. Atomicity(원자성), Consistency(일관성), Isolation(독립성), Durability(지속성) 4가지를 일컫는 말이다.

  • Atomicity: 위에서 나왔던 atomic 이라는 단어와 같은 의미다. 트랜잭션 안에 있는 연산은 반드시 모두 실패하거나 성공한다.
  • Isolation: 하나의 트랜잭션을 수행하고 있을 때 다른 연산이 끼어들 수 없다.
    • 읽기, 쓰기 모두 불가능하다.
  • Durability: 성공적으로 수행된 트랜잭션은 영원히 반영되어야 한다.
    • 트랜잭션 실행 결과가 갑자기 그 이전으로 되돌아가거나, 저장된 내용이 날아가면 안된다.

위 3가지 속성은 데이터베이스가 가지는 속성이 맞다. 그런데 Consistency는 그렇지 않다.

ACID는 사실 ACID가 아니다

  • Consistency: 트랜잭션이 성공적으로 수행되었다면 데이터베이스는 항상 일관된 상태를 유지한다.
    • 예) "휴대폰 번호는 항상 '010'으로 시작한다."는 제약 조건을 만족해야 한다.

이런 속성은 DB가 제어할 수 있는게 아니다. 데이터베이스로 쓰기 요청을 보내는 애플리케이션쪽에서 조건을 만족하는 값인지 확인을 하고 DB에 넘겨줘야 한다. 물론 일관성을 보장하기 위한 최소한의 장치는 데이터베이스에도 있다. 키에 대한 외래키 조건, 유일성 조건 등이 있다. 하지만 완벽한 일관성을 보장하는 주체는 데이터베이스가 아닌 애플리케이션이다.

책에는 이런 구절과 함께, ACID가 결국 마케팅 용어로 쓰였다는 내용도 나온다.

따라서 C는 실제로 ACID에 속하지 않는다.
각주) 조 헬러스테인은 하더와 로이터의 논문에서 ACID의 C는 "약어를 만들기 위해 끼어들"었고 당시에는 일관성이 중요하게 생각되지 않았다고 말했다.

트랜잭션 관련 키워드

단일 객체 연산과 다중 객체 연산

여기서 말하는 객체는 테이블, 도큐먼트, 레코드 등을 말한다. 단일 객체 연산은 말 그대로 객체 1개에 대한 연산을, 다중 객체 연산은 객체 여러개에 대한 연산을 말한다. 예를 들어 SNS에서 회원가입 처리를 할 때 회원 테이블 하나만 업데이트한다면 단일 객체 연산을 한 것이고, 다른 테이블도 같이 업데이트를 한다면 다중 객체 연산을 한 것이다.

Abort와 best effort

트랜잭션이 ACID를 보장하기 위해 어느정도로 노력하는지를 나타내는 대표적인 단어다. 아주 강한 정도로(그리고 이게 원래는 기본값이었을 것이다.) ACID를 보장하는 데이터베이스라면 보통 잘못된 트랜잭션 연산을 허용하지 않는다. 즉, abort한다. 반면 어느정도 잘못된 연산을 허용하는 경우를 'best effort를 한다'라고 표현한다.

데드락을 예로 들어보자. 데드락 자체가 절대 발생하지 않게 막는 데이터베이스는 문제의 원인이 되는 트랜잭션을 abort 시킬 것이다. 반면 데드락 발생을 막기 위해 best effort를 하는 데이터베이스는 데드락의 발생 자체를 막지는 않는다. 다만 데드락 발생을 감지해서 그 상황을 빠져나오는 방법을 제공할 것이다.

경쟁 조건 race condition

둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태.

경쟁 조건이라는 단어 자체는 컴퓨터 공학에서 광범위하게 쓰인다. 이 단어를 볼 수 있는 가장 대표적인 분야로 운영체제가 있다. DB에서 말하는 경쟁 조건은 트랜잭션이 원인이 된다. 클라이언트가 DB에서 할 수 있는 연산은 크개 2가지로 나뉜다. 읽기와 쓰기. 그리고 이 연산이 복잡하게 얽혀서 다양한 경쟁 조건을 만들어낸다. 책에서 ACID에 대해 살펴본 다음에는 이 경쟁 조건을 꽤 자세히 다룬다.

책에 나온 경쟁 조건을 나열해보자면...

  • 읽기 스큐 read skew: 비반복 읽기 nonrepeatbale read 라고도 부른다. 같은 읽기 요청을 2번 보냈을 때, 일시적으로 서로 다른 부분을 보게 되는 경우.
  • 쓰기 스큐 write skew: 트랜잭션이 무언가를 읽은 뒤 그 값을 기반으로 어떤 결정을 해서 그 결정을 DB에 쓴다. 그러나 쓰기를 실행하는 시점에서는 결정의 전제가 더이상 참이 아닌 경우.
  • 팬텀 읽기 phantom read: 트랜잭션이 어떤 검색 조건에 부합하는 객체를 읽고 있을 때, 다른 클라이언트가 그 검색 결과에 영향을 주는 쓰기를 실행하는 경우.
  • 더티 읽기 dirty read: 한 클라이언트가, 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 읽는 경우.
  • 더티 쓰기 dirty write: 한 클라이언트가, 다른 클라이언트가 썼지만 아직 커밋되지 않은 데이터를 덮어쓰는 경우.
  • 갱신 손실 lost update: 두 클라이언트가 도잇에 read-modify-write 주기를 실행할 때, 한 트랜잭션이 다른 트랜잭션의 변경을 포함하지 않은 채 그 내용을 덮어써서 데이터가 손실되는 경우.

...이정도가 된다. 이 책의 좋은 점은 각각의 경우에 대한 정의를 깔끔하게 적어놓았고, 어떤 기법을 써서 해결할 수 있으며 그 해결책의 장단점 및 부작용까지 모두 나와있다는 점이다. 가끔 기술 서적을 읽다보면 필요한 곳에서 용어의 정의를 제대로 설명해주지 않아 직접 구글링을 해야 하는 경우가 생겼는데, 이 책은 그런게 없어서 좋았다.

경쟁 조건을 해결하는 방법: 트랜잭션 격리

각 트랜잭션이 서로의 작업에 침범할 수 없도록 격리하는 방법도 나온다. 많이 나온다. 너무 많이 나와서 조금 정신이 없다. 각 방법이 어떤 경쟁 조건을 해결할 수 있는지도 나온다. 이 책을 처음 읽는 지금으로서는 우선은 각각의 방식을 이해하고 넘어가는걸 목표로 했다. (미래의 내가 각 방법과 경쟁 조건을 연결하는 작업도 해줄거라고 믿는다.😇)

2가지 격리 수준

  • 완화된 격리 수준: 성능은 좋지만 다양한 경쟁 조건에 취약하다.
  • 직렬성 격리: 경쟁 조건을 없애준다. 다만 1가지 방법을 빼고, 성능이 좋지 않거나 확장이 잘 되지 않는 문제를 갖고 있다.

완화된 격리 수준

Read, write 동시 실행 시 read가 무엇을 볼 수 있는지를 정하는 방법

  • 커밋 후 읽기 read committed: dirty read/write을 방지한다.
    • dirty write 방지를 위한 locking과
    • dirty read 방지를 위한 locking 혹은 '기억하고 기다리기' 기법을 써서 구현한다.
  • 스냅숏 격리 snapshot isolation: read skew를 해결한다.
    • 다중 버전 동시성 제어 (MVCC, Multi-Version Consistency Controll)를 사용한다.
    • write 연산에는 락을 걸지만 read 연산에는 아무 제약도 걸지 않는다. 여기에 가시성 규칙을 곁들이면 스냅숏 격리가 된다.
    • 왜인지 모르겠지만 DB마다 서로 다른 표현을 사용한다고 한다. (오라클: 직렬성 / 포스트그레스큐엘 & MySQL: 반복 읽기)

갱신 손실 방지

갱신 손실은 꽤 흔한 문제다. 해결하기 위한 방법도 다른 경쟁 조건에 비해 비교적 다양하다. 2가지 방식이 있다. 갱신 손실 자체를 막는 방식과 갱신 손실을 허용하고 나중에 어떻게든 잘 수습하는 방식이다.

  • 손실 허용 X: 원자적 쓰기, 명시적 잠금, 최종 쓰기 승리
  • 손실 허용 O: 갱신 손실 자동 감지, Compare-and-set

직렬성 격리

직렬성 격리는 경쟁 조건을 푸는 가장 강력한 방법이다. 그럴 수밖에 없다. 경쟁 조건이 만들어질 수 있는 환경을 아예 원천봉쇄해버리기 때문이다.

단일 스레드를 사용해서 진짜로 직렬로 실행하기

그러니까 멀티스레딩 기법을 쓰지 않고 진짜로 정확히 '동시에' 실행되는 스레드가 없도록 하는 것이다. 데이터베이스 연산은 그 어떤 순간에도 단 하나만 실행된다. 이렇게 간단하고 강력한 방법을 왜 모든 데이터베이스에서 쓰지 않는 걸까? 정답은 확장성 때문이다. 확장성을 포기할 수 없는 애플리케이션이라면 이 방식으로는 절대 충분하지 않다.

2PL: two-phase locking

이 단어도 운영체제에서 본 적 있는 친구다. 트랜잭션은 락(lock)을 소유할 수 있으며, 락의 종류는 shared, exclusive 모드 2가지가 있다. 그래서 two-phase locking이라고 부른다. 이 락을 구현하는 방식도 다양하다. 책에서는 predicate, index-range, next-key locking에 대해 설명한다.

직렬성 스냅숏 격리 serializable snapshot isolation, SSI

비교적 최근에 나온, 아주 유망한 방법이라고 한다.

'낙관적 방법'을 사용해서 트랜잭션이 차단되지 않고 진행할 수 있게 한다. 트랜잭션이 커밋을 원할 때 트랜잭션을 확인해서 실행이 직렬적이지 않다면 어보트시킨다.


8장:분산 시스템의 골칫거리

다음 장에서는 문제가 좀 더 심각해진다. 트랜잭션과 관련된 데이터베이스 자체 결함에서 벗어나서 네트워크와 시계를 의심한다. 모든게 잘못 될 수 있다는 가정을 만드려는 것 같다. 무서운건 모두 현실에서 생길 수 있는 시나리오라는 점이다. 😇

profile
코드도 적고 그림도 그리고 글도 씁니다. 넓고 얕은 경험을 쌓고 있습니다.

0개의 댓글