일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- Google OAuth
- spring security
- OAuth 2.0
- 일급 객체
- lombok
- Volatile
- nestjs
- java
- middleware
- 일급 컬렉션
- Dependency Injection
- synchronized
- factory
- Spring
- builder
- Today
- Total
HJW's IT Blog
PostgreSQL 격리 수준 제대로 이해하기: MVCC·VACUUM·SSI 본문
0. 사전 지식
대부분 알고 있겠지만, DBMS 의 트랜잭션 격리 수준은 4가지로 나뉜다.
- READ UNCOMITTED: 다른 트랜잭션이 commit 하지 않은 데이터도 보인다. Dirty Read 발생 가능
- READ COMITTED: 다른 트랜잭션이 commit 한 데이터만 보인다. 하지만 트랜잭션 실행 도중 데이터가 변경될 수 있다. Non Repeatable Read 발생 가능
- REPEATABLE READ: 트랜잭션에서 하나의 row 를 두번 조회하였을때 결과가 같음을 보장한다. 하지만 다건 조회의 경우 다를 수 있다. Phantom Read 가능
- SERIALIZABLE: 락을 거는 등, 고수준의 방법을 통해 한번에 하나의 트랜잭션만 해당 데터를 다룸을 보장한다.
이러한 '격리'는 데이터의 무결성을 보장하기 위한 ACID 의 Isolation 을 달성하기 위한 방식으로, 여러 트랜잭션이 동시에 실행 되더라도 독립적으로 동작하도록 보장하는 속성이다. 즉, 동시성 문제를 다루기 위해선 이러한 격리 수준에 대한 이해가 필수적이다.
1. PostgreSQL MVCC
MVCC
란 Multi-Version Concurrency Control 을 의미하며, PostgreSQL 의 경우, 단순 SQL 표준의 격리 수준 이상의 의미를 지닌다.
1.1 MVCC 의 원리
MVCC
는 동시성 제어를 위해 읽기 트랜잭션이 쓰기 트랜잭션을 차단하지 않고, 그 반대도 마찬가지인 비차단 철학을 기반으로 동작한다. 전통적인 DBMS 의 경우, 락 기반의 시스템으로, 하나의 트랜잭션이 쓰기 작업을 하는 도중 해당 row 에 대해 exclusive lock 을 걸어 읽기조차 차단한다. 하지만 MVCC
의 경우, 데이터의 여러 버전을 유지하여 각 트랜잭션이 독립적인 일종의 스냅샷을 보고 작업할 수 있도록 한다.
전통적인 DBMS의 UPDATE 혹은 DELETE 는 원본 row 를 가지고 동작한다. 즉 원본이 수정/삭제가 되는 것이다. PostgreSQL에선 조금 다르다. 각 행은 xmin
(행을 생성한 트랜잭션 ID) 와 xmax
(행을 수정한 트랜잭션 ID) 에 대한 메타 데이터를 보유한다. 이러한 메커니즘을 이용해 PostgreSQL의 기존 행은 '만료됨'으로 표기하고 새로운 row 버전을 삽입함으로서 읽기 작업이 쓰기 작업에 의해 차단되지 않도록 한다. 즉 다중 사용자 환경에서 높은 동시성과 성능을 챙길 수 있는것이다.
1.2 튜플 버전 관리
PostgreSQL 은 이전에 설명한 것과 같이 두개의 숨겨진 컬럼을 보유한다.
xmin
: row를 생성한 트랜잭션 IDxmax
: row를 만료시킨 트랜잭션 ID
트랜잭션이 시작되면, PostgreSQL은 그 시점의 활성 트랜잭션 목록을 포함하는 스냅샷을 생성한다. 이로서 어떤 행 버전이 해당 트랜잭션에게 보일지 결정할 수 있다.
- 행 버전의
xmin
트랜잭션이 이미 커밋 되었는지 - 행 버전의
xmax
가 0(null) 이거나, 스냅샷이 생성될 당시 아직 커밋되지 않은 행
이를 통해 PostgreSQL의 모든 트랜잭션은 자신이 시작된 시점의 커밋된 데이터만 볼 수 있으며, 중간에 변경된 사항은 절대 볼 수 없다. 즉, Dirty Read 가 MVCC 에 의해 원천적으로 차단되는 것이다.
1.3 VACUUM
위와 같이 row 를 실제로 삭제하지 않고 새로운 버전을 생성하는 방식이라면, 시간이 지남에 따라 'dead tuple'들이 누적되게 된다. 이 현상을 bloat
라고 하며, 테이블의 크기를 불필요하게 증가시켜 성능 저하로까지 이어질 수 있다.
이 문제를 해결하기 위해 PostgreSQL은 VACUUM
을 사용한다.
VACUUM
은 더이상 어떠한 트랜잭션도 접근할 수 없는 죽은 튜플이 차지하는 공간을 회수하는 과정으로, 만약 이러한 VACUUM
이 주기적으로 실행되지 않는다면, bloat
현상이 심해져 성능의 저하로 이어진다.
VACUUM
은 다음과 같은 동작을 한다.
Dead Tuple 을 정리하여 Free Space Map 으로 반환
VACUUM
이 정리한 공간에 대해 짚고 넘어가야 할 점은, 정리된 공간들은 OS에 반환되는 것이 아니라, 테이블 내의 FSM 에 재사용 가능한 공간으로 등록된다. 즉, 이후 새로운 행이 삽입될때 해당 공간을 재사용함으로서 동작한다.
Transaction ID Wraparound 방지
또한 이전 언급한 xmin, xmax
는 32비트 정수형으로 관리되는데, 약 40억개의 트랜잭션마다 한번씩 재사용 된다. 만약 VACUUM
이 너무 오랜 시간 동작하지 않아 이 id 값이 오래되면, 데이터의 논리적 무결성이 깨질 수 있다. (새로운 id 가 오래된 id보다 저 작게 인식)
이 문제를 해결하기 위해 PostgreSQL은 freeze
를 사용한다. 이 과정을 통해 오래된 id 값을 특별한 값으로 변경하여 항상 새로운 id 값 보다 오래된 것으로 인식하게 한다.
통계 정보 갱신
VACUUM
은 실행될때 ANALYZE
와 함께 실행되어 테이블의 데이터 붙포에 대한 통계 정보를 갱싱할 수 있다. 쿼리 플래너는 이렇게 갱신된 통계 정보를 사용하여 최적의 쿼리 실행 계획을 수립한다.
Index scan 성능 향상
마지막으로 VACUUM
은 Visibility Map
이라는 내부 자료구조를 갱신하는 역할을 한다. 이 VM
은 특정 페이지의 모든 행이 모든 활성 트랜잭선에게 보이는지를 기록하는 역할을 하는데, 이전과 마찬가지로 쿼리 성능을 최적화 할 수 있다.
PostgreSQL이 Index-Only Scan 을 수행할 때, 이 VM
을 참조하여 all-visible
로 마킹된 페이지는 전체적인 과정을 생략하고 인덱스에서 직접 읽어올 수 있어 I/O를 크게 줄일 수 있다.
2. PostgreSQL 격리 수준
다른 DBMS 와 다르게 PostgreSQL 은 3가지 격리 수준으로 구분될 수 있다. MVCC
에 의해 READ UNCOMMITTED 와 READ COMITTED 가 동일하게 동작하기 때문이다.
2.1 READ COMMITTED
READ COMMITTED 는 PostgreSQL이 제공하는 기본 격리 수준으로, 각 쿼리 가 실행될 때 마다 새로운 스냅샷을 생성한다. 그렇기 때문에 하나의 트랜잭션 내에서 여러 쿼리를 실행할 경우, 그 사이에 다른 트랜잭션이 commit 한 변경사항을 볼 수 있다.
- Non-repeatable read, Phantom read 발생 가능
2.2 REPEATABLE READ
REPEATABLE READ 의 격리 수준에선, 트랜잭션이 시작되는 시점에 생성된 스냅샷이 해당 트랜잭션이 끝날때 까지 유지된다. 즉, 하나의 트랜잭션 내의 모든 쿼리는 동일한 snapshot 을 바라보게 된다. 이러한 특성 덕분에 Non-repeatable read 뿐만 아니라 Phantom read 까지도 방지할 수 있다.
2.3 SERIALIZABLE
PostgreSQL의 REPEATABLE READ 는 Phantom Read 까지도 방지할 수 있다. 그렇다면 SERIALIZABLE 은 언제 필요할까?
바로 WRITE SKEW
현상을 방지하기 위해서이다. 예를 들어 좌석 예약 시스템을 생각해 보자. 한 좌석에는 한사람만 예약할 수 있다. 이때, 동시에 같은 좌석을 예메하는 두개의 요청이 들어와 두개의 별도의 트랜잭션으로 실행될 경우, REPETABLE READ 수준의 격리에선 두개의 트랜잭션이 자신의 스냅샷에선 성공하는 논리적 팬텀이 발생하게 된다.
-- T1
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM reservations WHERE slot = '10:00'; -- 0 (스냅샷 기준)
INSERT INTO reservations(user_id, slot) VALUES (1, '10:00');
COMMIT;
-- T2 (동시에)
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM reservations WHERE slot = '10:00'; -- 0 (자기 스냅샷 기준)
INSERT INTO reservations(user_id, slot) VALUES (2, '10:00');
COMMIT;
-- 두개의 트랜잭션 모두 성공
이때, 만약 SERIALIZABLE 수준의 격리를 걸어두었다면, 직렬성을 위반할 수 있는 충돌로 감지하여 (ww-dependency cycle) 하나의 트랜잭션을 SQLSTATE 40001
오류화 함께 롤백시킨다.
PostgreSQL은 위와 같이 잠금 기반의 직렬화 대신, SSI
(Serializable Snapshot Isolation) 을 사용한다. 이를 통해 MVCC
기반의 동작을 유지하면서도, 쓰기-쓰기 의존성을 지속적으로 추적하여 충돌을 감지할 수 있는것이다.
3. SSI
이전에 언급하였듯, PostgreSQL은 기본적으로 락 기반의 직렬화가 아닌 SSI
을 사용하여 트랜잭션의 직렬성을 보장한다.
3.1 SSI 동작 원리
PostgreSQL 의 REPEATABLE READ 는 Phantom Read 까지 방지 할 수 있다. 하지만 Write Skew 문제까지 해결하진 못한다. 이를 해결하기 위해 SERIALIZABLE 수준의 격리를 활용할 수 있고, SSI는 이를 트랜잭션간 의존성을 추적하여 처리한다.
SSI는 두가지 의존성을 모니터링한다
- rw-dependency: 한 트랜잭션이 데이터를 읽은 후, 다른 트랜잭션이 해당 데이터를 수정하는 경우
- ww-dependency: 두 트랜잭션이 동일한 데이터에 대해 쓰는 경우
PostgreSQL은 이러한 dependency cycle 이 감지되면, 둘 중 하나의 트랜잭션을 롤백시켜 데이터의 정합성을 보장한다.
3.2 SSI의 대안
SSI
를 활용하면, 강력한 직렬성을 보장하지만 추가적인 오버헤드가 발생한다. 예를 들어 의존성을 추적하기 위한 추가적인 자원 소모, 직렬성 위반 가능성 과대평가로 인한 불필요한 롤백, 혹은 장기 트랜잭션의 경우, 많은 의존성을 생성하여 충돌 및 롤백 가능성의 증가등의 문제가 있다.
이를 어느정도 대채하기 위해 MVCC 를 일시적으로 우회하여 테이블의 행에 락을 거는 SELECT FOR UPDATE
를 활용할 수 있다. 특정 행에 exclusive lock 을 걸어 다른 트랜잭션으로부터의 무결성을 보장하는 방법이다.
당연하게도 SERIALIZABLE과 SELECT FOR UPDATE 는 배타적 개념은 아니다
4. 결론
PostgreSQL의 트랜잭션 격리 수준은 단순히 SQL 표준을 준수하는 것을 넘어, MVCC 아키텍처덕 분에 Dirty Read
와 같은 현상이 원천적으로 방지되며, REPEATABLE READ
수준에서 Phantom Read
까지 막는 등 SQL 표준을 뛰어넘는 강력한 일관성을 제공한다.
데이터베이스 설계자와 개발자는 이러한 PostgreSQL의 MVCC 작동 원리를 깊이 이해하고, 애플리케이션의 데이터 무결성 요구사항과 성능 특성을 종합적으로 고려하여 적절한 격리 수준을 선택해야 한다. 대부분의 경우 READ COMMITTED
로 충분하지만, 일관된 데이터 스냅샷이 필요한 경우 REPEATABLE READ
를, 최고의 무결성이 요구되는 경우 SERIALIZABLE
을 고려할 수 있다. 이와 더불어 SELECT... FOR UPDATE
와 같은 명시적 잠금은 낮은 격리 수준에서도 특정 작업의 정확성을 보장하는 유용한 도구가 된다.
'Database' 카테고리의 다른 글
[Database Studio] 2주 수업 정리 (0) | 2024.05.26 |
---|---|
Complex Query 2 (0) | 2023.09.22 |
DB: Chapter 7(Complex Queries) (0) | 2023.09.16 |
DataBase: 3주 (0) | 2023.09.09 |
Database: 1주차 (0) | 2023.08.29 |