Database 구축과 사용을 위한 ACID와 Transaction 의 이해

ACID 는 데이터베이스의 중요한 속성으로 여겨집니다.
이번 포스팅에서는 각각의 속성과 Transaction에 대해 얘기해보도록 하겠습니다.
Transaction
ACID에 대해 얘기하기 전, 데이터베이스 트랜잭션이 무엇인지를 확실하게 이해해야합니다.
단순하게 정의해봅시다.
SQL 쿼리들을 하나의 작업 단위로 취급하는 이유는 SQL이라는 언어의 특성 때문입니다.
구조화되어 여러 테이블에 분산되어있는 데이터를 가지고는 하나의 쿼리 내에 원하는 모든 작업을 처리하기 어렵습니다.
아주 단순화시킨 예를 들어보겠습니다.
A 라는 사용자가 포인트를 이용해 마트에서 물건을 사는 과정을 시나리오로 설정해보겠습니다.
수행해야하는 쿼리는 아래와 같습니다.
- A 계좌에서 물건을 구매하기에 충분한 포인트가 있는지 확인(SELECT)
- A 계좌에서 물건 가격만큼 포인트 차감(UPDATE)
- 마트 장부에 해당 물건 가격의 포인트 추가(UPDATE)
위 3개의 쿼리는 어쩔 수 없이 여러 쿼리를 거칠 수 밖에 없겠죠.
이들을 하나의 작업단위로 묶어야만 합니다. 하나의 트랜잭션이라고 표현할 수 있습니다.
Transaction 의 수명
트랜잭션은 트랜잭션의 시작을 명시하는 키워드인 "BEGIN" 으로 시작됩니다.
그리고 원하는 모든 쿼리들이 수행되면, 우리는 "COMMIT" 키워드로 현재까지의 변경사항을 영구적인 디스크에 저장할 수 있게 됩니다.
만약 쿼리 수행 중, 현재의 변경사항들을 저장하고 싶지 않고 되돌려야한다면 "ROLLBACK" 키워드를 사용할 수 있습니다.
Atomicity (원자성)
원자성이 의미하는 것은 아래와 같습니다.
하나의 트랜잭션을, 더 이상 나눌 수 없는 원자와 같이 사용하는 것입니다.
위의 A라는 사용자가 포인트를 이용해서 마트에서 물건을 사는 과정을 다시 한번 가져와 보도록 하겠습니다.
만약 2번 UPDATE 를 수행한 이후 문제가 생겼는데 아무런 롤백 과정이 없다면, A 계좌는 포인트 차감만 되어버리는 억울한 상황이 발생할 수 있겠죠.
문제가 생겼다면, 트랜잭션 내 모든 쿼리들을 처음과 같은 상태로 되돌려야합니다.
이게 원자성의 룰입니다.
그런데, 만약 커밋을 하기 전 데이터베이스가 다운되어버리면 어떻게 될까요?
데이터베이스가 다운되어서 롤백할 수 없는 경우가 되어버려 데이터에 영영 문제가 생겨버리는 걸까요?
데이터베이스는 다시 재실행될 때 이전까지 실행되고 있었던 트랜잭션이 있음을 확인하고 해당 트랜잭션을 실패했다고 처리해야합니다.
Consistency (일관성)
일관성이 의미하는 것은 아래와 같습니다.
일관된 상태? 이게 무엇을 말하는 걸까요?
여기서 말하는 "일관된 상태"란, 데이터베이스의 제약 조건, 관계, 유효성 검사 등을 준수함을 의미합니다.
예를 들어, 사람의 나이를 저장하는 컬럼은 음수가 되지 말아야합니다.
이러한 조건이 깨진다면, 이것은 일관성이 깨진 상태라고 얘기할 수 있습니다.
일관성은 크게 두 가지로 나뉩니다.
1. 데이터자체의 일관성
이는 현재 유지되고 있는 데이터의 상태를 나타냅니다.
실제 디스크에 적혀있는 것과, 우리가 설정한 데이터 모델이 일치하는지 확인해야합니다.
예를 들어보겠습니다.
아래는 좋아요 기능이 있는 다이어리 테이블입니다.
1번 일기는 좋아요 1개, 2번 일기는 좋아요 2개를 얻은 상황입니다.
ID | TITLE | CONTENT | LIKES |
---|---|---|---|
1 | 오늘 하루도 화이팅! | ... | 1 |
2 | 커피를 그만 마셔야할텐데요.. | ... | 2 |
아래는 유저 별로 어떤 일기 좋아요를 눌렀는지 알 수 있는 테이블 입니다.
1번 유저는 1번 일기에 좋아요를, 2번 유저와 3번 유저는 2번 일기에 좋아요를 눌렀습니다.
USER_ID | DIARY_ID |
---|---|
1 | 1 |
2 | 2 |
3 | 2 |
만약 다이어리 테이블의 LIKES 개수와 좋아요 정보 테이블의 개수가 맞지 않는다면 어떻게 될까요?
ID | TITLE | CONTENT | LIKES |
---|---|---|---|
1 | 오늘 하루도 화이팅! | ... | 1 |
2 | 커피를 그만 마셔야할텐데요.. | ... | 2 |
USER_ID | DIARY_ID |
---|---|
1 | 1 |
2 | 1 |
3 | 2 |
유저 2번이 다이어리 아이디 1번에 좋아요를 눌렀으나, 다이어리 테이블에는 좋아요 수가 반영되지 않았습니다.
이 경우, "일관성이 깨졌다" 는 표현을 쓸 수 있습니다.
2 . 일관된 읽기
데이터가 디스크 내에서는 일관성을 유지할 수 있지만, 데이터의 읽기는 여러 인스턴스가 동기화되지 않아 일관성이 사라질 수 있습니다.
이는 시스템 전체의 구성도를 봐야합니다.
데이터베이스가 하나이고, 이 데이터베이스에서 읽기와 쓰기를 모두 수행한다고 가정했을 때 특정 UPDATE 쿼리를 수행하고 커밋하면, 추후 읽었을 때 UPDATE 된 내용이 반환되게 됩니다.
그런데, 현업에서 데이터베이스를 다루다보면 많은 데이터베이스 시스템이 읽기와 쓰기 인스턴스가 분리되어있는 것을 확인할 수 있습니다.
부하를 분산시키기 위함이죠.
만약 이러한 환경(읽기와 쓰기 인스턴스가 분리되어있는 환경)에서 유저가 쓰기 인스턴스에 UPDATE하고, 이 내용이 읽기 인스턴스로 복제되기 이전에 유저가 읽기 인스턴스에 읽기를 요청하면 어떻게 될까요?
이는 결국 일관성이 깨지는 것을 의미합니다.
이것은 앞서 얘기했던 데이터에서의 일관성과는 조금 다른 느낌이죠.
시스템 전체를 봐야한다는 얘기가 바로 이런 맥락에서 한 얘기입니다.
이 개념은 "최종적 일관성" 이라는 말과도 연관이 있는데요.
최종적 일관성이란, 지금은 일관성이 없지만 결국 일관성을 가지게 되는 것입니다.
보통 정상적인 시스템이라면 쓰기 인스턴스에 UPDATE 된 내용이 읽기 인스턴스로 빠르게 적용되기 때문에, 시간이 흐른 후 읽는다면 데이터는 일관적이게 됩니다.
이게 바로 최종적 일관성입니다.
다만, 최종적 일관성은 데이터의 일관성이 깨질 때는 적용되지 않는 말입니다.
아무리 기다려도 특별한 수정작업이 일어나지 않는 한 영영 데이터는 불일치하기 때문입니다.
Isolation (고립성)
여러 트랜잭션이 동시에 동일한 데이터를 업데이트하면 어떻게 될까요?
이 질문에서 고립성의 필요를 짐작할 수 있습니다.
고립성이 의미하는 것은 아래와 같습니다.
처음의 질문에 답을 해보면, 각 트랜잭션은 서로가 서로에게 영향을 주지 않고, 동시에 실행되는 다른 트랜잭션에 영향을 받지 않아야합니다.
각각의 트랜잭션들이 완전히 분리, 고립되는 것이죠.
사실 이 고립성은 유령 읽기 현상, 여러 가지의 고립 수준(Isolation Level) 개념이 중요하지만, 내용이 길어질 것 같아 이번 포스팅에서는 다루지 않겠습니다.
Durability (지속성)
지속성이 의미하는 것은 아래와 같습니다.
디스크에 저장하므로, 전력이 나가거나 트랜잭션을 완료한 후 클라이언트 충돌이 발생하더라도, 다시 돌아와 변경 사항을 볼 수 있습니다.
지속성은 느립니다.
이를 유지하기 위해 디스크에 저장시켜야하고, 기본적으로 디스크 쓰기는 속도가 느리기 때문이죠.
그래서 많은 데이터베이스가 이 지속성을 고려하여 메모리에 쓰고, 백그라운드에서 스냅샷을 찍고, 이를 디스크에 기록한다고 말합니다.
지속성을 유지시키기 위한 다양한 기술들
이 지속성을 유지시키기 위해서 데이터베이스는 다양한 기술들을 사용합니다.
1. WAL(Write-Ahead Logging)
트랜잭션이 커밋되기 전 해당 트랜잭션의 변경 사항을 로그 파일에 기록합니다.
이후에는 이 로그 파일을 순서대로 읽어가며 변경 사항을 디스크에 저장합니다.
만약 트랜잭션 과정 중 크래시가 발생한다면 우리는 이 WAL 을 활용해서 다시 지속성을 위한 디스크 쓰기를 진행할 수 있습니다.
2. Asynchronous snapshot
우리가 쓰는 동안 모든 것을 메모리에 유지하는 기술입니다.
그런 다음 백그라운드에서 비동기적으로 한꺼번에 모든 것을 디스크에 넣습니다.
Redis 가 이러한 방식을 쓰고 있습니다.
마무리
오늘 이렇게 데이터베이스의 가장 중요한 속성인 ACID 와 트랜잭션에 대해 알아봤습니다.
다음 포스팅에서는 오늘 언급되었던 Isolation에 대해 조금 더 자세히 알아보도록 하겠습니다.