개요
내가 백엔드 개발자라면, 또는 DB를 공부해 본 적이 있다면 한 번쯤은 ACID에 대해 들어봤을 것이다. 생각해보면 DB를 공부하며, 또는 개발 면접을 준비하며 ACID를 달달 외운 적은 있어도, 각각의 특성을 어떻게 DBMS가 보장해주는지를 찾아본 적은 없는 것 같다.
오늘은 트랜잭션, ACID에 대해 알아보고, MySQL이 ACID 각각의 특성을 어떻게 보장해주는지까지 공부해보자 💪
트랜잭션이란?
데이터베이스 트랜잭션(Database Transaction)은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위이다. 여기서 유사한 시스템이란 트랜잭션의 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미한다.
- 위키백과 : 데이터베이스 트랜잭션 문서
^^ 봐도 잘 모르겠다. 간단한 예시와 함께 살펴보자. 우리는 붕어빵을 사먹으러 포장마차에 들렀고, 포장마차엔 이미 먼저 온 손님이 있다. 우리를 A, 먼저 온 손님을 B라고 부르자. 붕어빵은 슈크림 1개, 팥 1개 총 2개 남아있는 상황이다.
한 번 붕어빵을 구입해보자! 붕어빵 구입에 필요한 과정은 다음과 같다.
1. 어떤 붕어빵을 구입할 지 고른다
2. 판매원에게 돈을 준다
3. 판매원에게서 붕어빵을 받는다
아주 간단하다. A는 고민 끝에 슈크림 붕어빵을 구입하기로 결정하고 판매원에게 슈크림 붕어빵 가격을 지불한다. 하지만 판매원은 당황하며 이 요청을 거절한다. A가 고민하는 사이 B가 1개 남은 슈크림 붕어빵을 이미 구입했기 때문이다.
즉, 이는 붕어빵이 A와 B 모두가 동시에 접근할 수 있는 공유 자원이기 때문에 발생한 현상이다. 지금은 오프라인에서 직접 붕어빵의 잔여 개수와 다른 손님의 요청 (=주문)을 들을 수 있지만, 붕어빵 가게가 아주 커져서 DB로 재고 데이터를 관리하게 되었다고 가정하면 얘기가 달라진다. 위에서 살펴본 붕어빵 구입 과정을 DB 친화적으로 바꿔보자.
1. READ (붕어빵 재고)
2. UPDATE (가게 판매액)
3. UPDATE (붕어빵 재고)
앞에서 발생한 A의 상황을 위 과정에 대입해보면, A는 1, 2번 쿼리 요청에 성공했으나 3번 쿼리 요청엔 실패했다. 이 경우 두 가지 문제가 생긴다.
1. 분명 1번 쿼리에서 붕어빵 재고를 확인했는데, 왜 3번 쿼리가 실패하는가?
2. 붕어빵 구매에 실패했으므로, 2번 쿼리의 결과를 롤백해야 한다 (= 판매원에게 다시 돈을 돌려받아야 한다)
이 결과를 통해, 우리는 처리 중인 데이터를 다른 곳에서 조회할 때, 그리고 처리 중인 작업이 실패했을 때 데이터 정합성이 깨질 수 있음을 알 수 있다. 그리고 이런 데이터 정합성이 깨지는 문제를 보완하기 위해, 한 가지 아이디어를 낼 수 있다.
붕어빵 구입에 필요한 3 단계를 하나의 작업인 것처럼 취급하면, 다른 곳에서 동시에 조회하는 문제와 실패했을 때 롤백하는 문제를 쉽게 처리할 수 있지 않을까?
트랜잭션은 이렇게 등장했다. 예시를 통해 정리한 트랜잭션의 정의는 다음과 같다.
DB의 상태를 변환하는 하나의 논리적 기능(= 붕어빵 구입)을 수행하기 위한 일련의 상호작용(= 붕어빵 재고 확인, 결제, 붕어빵 재고 수정)의 단위
트랜잭션의 특성, ACID
트랜잭션은 데이터 정합성을 유지하는 4가지 특성을 갖는데, 이를 각 특성의 앞 글자를 따서 ACID라고 부른다. 하나씩 살펴보자!
1️⃣ Atomicity (원자성)
- 트랜잭션은 원자적 연산을 보장해야 한다.
- All : 전부 성공하여 완전히 데이터베이스에 반영된다.
- Nothing : 어느 하나라도 실패하면 해당 트랜잭션에 포함된 모든 연산은 실패하며, 데이터베이스에 아무것도 반영되지 않는다.
- 작업이 모두 반영되거나, 혹은 모두 반영되지 않는 원자성을 통해 우리는 트랜잭션의 결과를 예측할 수 있다.
2️⃣ Consistency (일관성)
- 트랜잭션이 성공적으로 종료되면 언제나 데이터 무결성이 보장된다.
- 데이터 무결성 : 제약 조건을 통해 지정된 기본키, 외래키, 도메인 규칙, 도메인 제약 조건(unique 등)이 항상 보장되어야 한다.
- 트랜잭션 전후, 데이터베이스는 제약과 규칙을 모두 만족하여 일관된 상태를 유지한다.
3️⃣ Isolation (독립성)
- 둘 이상의 트랜잭션이 동시에 실행되는 경우, 이 트랜잭션들은 서로 간섭하지 않고 독립적으로 동작한다.
- 어느 하나의 트랜잭션이 실행 중일 때, 다른 트랜잭션의 연산은 끼어들 수 없다.
- 또한, 실행 중인 트랜잭션이 종료될 때까지, 다른 트랜잭션은 이 실행 결과를 참조할 수 없다.
- 동시에 여러 트랜잭션들이 수행될 때, 각 트랜잭션은 서로 격리되어 있기에, 마치 연속으로 실행한 것 같은 결과를 낸다.
4️⃣ Durability (지속성)
- 성공적으로 완료된 트랜잭션의 결과를 유실하지 않는다.
- 만약 런타임 오류나 시스템 오류가 발생하더라도, 성공한 트랜잭션의 결과는 영구적으로 데이터베이스에 반영된다.
MySQL이 ACID를 보장하는 방법
위에서 우리는 트랜잭션의 4가지 특성을 살펴봤다. 위의 4가지 특성은 실제로 DBMS에 구현되어 그들의 트랜잭션을 안전하게 만들어준다. 이 신기한 특성들을 MySQL은 어떻게 구현한걸까? 하나씩 살펴보자.
1️⃣ Atomicity (원자성) 보장 방법
- MVCC (Multi Version Concurrency Control)를 이용할 수 있다.
- MVCC란?
- 일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능. 스냅샷을 이용해 동시성을 제어한다.
- UNDO Log 스냅샷을 이용해 한 레코드에 대한 여러 버전을 동시에 관리할 수 있다.
- Lock 없이 일관된 읽기를 제공하는데 제일 큰 목적을 갖지만, 변경 관리 및 롤백에도 사용된다.
- MVCC를 이용한 원자성 보장 방법
- 갱신 요청이 들어오면, 갱신 이전 값 (before 값)을 undo log에 복사한다 (= 스냅샷 생성).
- 만약 트랜잭션에 실패한다면, undo log에 있는 백업된 데이터를 Inno DB 버퍼 풀로 복구해 롤백한다.
- 트랜잭션에 성공해 commit이 되어도 undo log를 바로 삭제하지는 않는다.
: 해당 트랜잭션보다 먼저 시작했지만 더 나중에 끝나는, undo log가 필요한 트랜잭션이 있을 수 있기 때문!
: 이런 관련된 트랜잭션이 더 이상 없을때야 undo 영역을 삭제한다.
- MVCC란?
2️⃣ Consistency (일관성) 보장 방법
- 일관성 보장을 위해 다음 제약 조건을 지켜야 한다.
- unique 제약 조건 : 이 필드는 중복 값을 가질 수 없다.
- primary key 제약 조건 : not null과 unique 제약 조건의 특징을 모두 가진다.
- foreign key 제약 조건 : 다른 테이블과의 연결 고리 역할을 한다. 즉 다른 테이블의 값과 일치해야 한다.
- not null 제약 조건 : 이 필드는 null 값을 가질 수 없다.
- default 제약 조건 : 이 필드는 기본 값을 가지며, insert 할 때 값을 입력하지 않으면 기본값이 지정된다.
- innoDB는 이 제약 조건들을 insert, update, delete, transaction commit 때마다 검사한다.
- 검사 과정에서 위 제약 조건 중 하나라도 위반된 경우, 해당 작업은 실패되며 오류를 반환하고, 트랜잭션은 롤백된다.
- 이때 주의해야 할 건 foreign key (외래키) 제약 조건!
- 다른 값들과는 다르게, 외래키는 참조하는 다른 테이블에서 발생하는 연산의 영향을 받기 때문이다. 따라서 FK 제약조건은 크게 두 가지 접근 방식을 갖는다.
- 갱신 제한 : 다른 테이블이 참조하고 있는 필드는 update 또는 delete를 할 수 없다.
- 갱신 전파 : on delete cascade 또는 on update cascade를 이용, 상위 테이블의 값이 갱신될 때 참조하는 하위 테이블도 갱신할 수 있다. 이때 cascade 작업은 trigger와는 무관하다.
- 다른 값들과는 다르게, 외래키는 참조하는 다른 테이블에서 발생하는 연산의 영향을 받기 때문이다. 따라서 FK 제약조건은 크게 두 가지 접근 방식을 갖는다.
3️⃣ Isolation (독립성) 보장 방법
- 트랜잭션 격리 레벨 (Isolation Level)을 이용해 구현한다.
- DBMS별로 지원하는 격리 레벨이 조금씩 다르다.
- 많은 성능을 포기해야 하므로 개발자가 격리 레벨을 직접 제어할 수 있으며, 따라서 제일 유연성있는 특성이다.
- MySQL InnoDB가 제공하는 격리 레벨은 다음과 같다.
- READ UNCOMMITTED : 트랜잭션의 변경 내용의 commit이나 rollback 여부에 관계 없이 다른 트랜잭션에 보인다.
- READ COMMITTED : 트랜잭션에서 커밋이 된 변경사항만 다른 트랜잭션에서 읽을 수 있다.
- REPEATABLE READ : InnoDB 기본 격리 수준. MVCC를 위해 Undo 영역에 백업된 데이터를 이용, 동일 트랜잭션 내에서는 동일한 결과를 보여줌을 보장한다.
- SERIALIZABLE : 가장 엄격한 레벨로, 성능이 제일 떨어진다. 읽기 작업을 할 때도 공유 락을 획득하며, 따라서 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 접근할 수 없게 한다.
4️⃣ Durability (지속성) 보장 방법
- WAL을 이용할 수 있다 :: WAL이 뭔지 모른다면 여기 링크 클릭!
- WAL을 통해 로그를 남기면, 데이터베이스 등에 오류가 발생하더라도 로그를 이용해 작업을 마칠 수 있다.
- 마지막 체크포인트 시점부터 최근 로그까지 모든 로그를 탐색하면서, 어디서부터 어떤 트랜잭션들을 복구해야 하는지를 분석한다.
- 복구를 시작해야 하는 시점부터 장애가 발생한 시점의 직전까지 모든 로그를 REDO한다. (= 트랜잭션 재반영)
- 로그를 시간 내림차순으로 탐색하면서 필요에 따라 로그를 UNDO한다. (= 트랜잭션 롤백. 완료되지 않은 트랜잭션이 존재할 경우, 해당 트랜잭션이 변경한 페이지들을 원상복구하는 것을 의미)
- REDO, UNDO 복구의 필요성을 판단할 땐 로그 레코드의 LSN (Log Sequence Number) 식별자를 이용한다.
- 모든 페이지는 해당 페이지를 마지막으로 갱신한 로그의 식별자를 갖는다.
- 따라서 해당 로그의 LSN와 page LSN을 비교하면 로그의 적용 필요 여부를 판단할 수 있다.
- page LSN < log LSN : 해당 페이지는 반드시 로그로 복구되어야 한다.
- page LSN >= log LSN : 페이지는 이 로그보다 나중에 쓰인 로그로 갱신되었으므로, 복구될 필요가 없다.
- 따라서 지속성을 보장하기 위해선 로그가 필수적인데, 이런 로그를 남기는 과정에서 성능 저하가 발생한다.
- 커밋 성능을 극대화하기 위해, 일부 DBMS는 지속성을 일부 포기하는 옵션을 제공하기도 한다.
- MySQL InnoDB의 경우, innodb_flush_log_at_trx_commit 파라미터를 이용해 보다 느슨하게 로그를 작성할 수 있다.
마무리
이렇게 트랜잭션, ACID, 그리고 MySQL이 ACID를 보장해주는 방법에 대해 알아봤다. 오늘 포스팅을 작성하면서 MySQL의 동작 원리에 대해 좀 더 깊이 이해할 수 있었다. 그리고 처음 보는 내용도 꽤 있어서 찾아보길 잘했다고 생각했다 🤩
이번엔 프로젝트에서 사용하려고 생각 중인 MySQL에 대해 알아봤는데, 나중에 다른 DBMS를 이용할 기회가 생기면 해당 DBMS에서 ACID를 보장하는 방법도 찾아봐야겠다.
레퍼런스
- MySQL 공식 문서 : https://dev.mysql.com/doc/refman/8.1/en/
- [위키백과] 데이터베이스 트랜잭션 : https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4_%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98
- [DB기초] 트랜잭션이란 무엇인가? : https://coding-factory.tistory.com/226
- [데이터베이스] 트랜잭션의 ACID 성질 : https://hanamon.kr/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%98-acid-%EC%84%B1%EC%A7%88/
- DBMS는 어떻게 트랜잭션을 관리할까? : https://d2.naver.com/helloworld/407507
- Real MySQL 8.0 1권 : https://product.kyobobook.co.kr/detail/S000001766482
'Develop > Database' 카테고리의 다른 글
[MySQL] MySQL의 인덱스 탐구하기 1 (feat. B-Tree) (1) | 2023.10.14 |
---|---|
[MySQL] MySQL의 아키텍처와 성능 핵심을 알아보자 (0) | 2023.09.29 |