데이터베이스(Postgresql)의 Lock은 어떤식으로 동작하는걸까?
서론
프로젝트를 하는 도중 동시성을 고려해야할 문제가 있었다. 투두리스트의 순서를 조정할 때 특정 로직에 의해서 해당 날짜에 해당하는 모든 엔티티들에 대한 전체 재정렬 로직이 필요했고, 정렬하는 사이 다른 추가적인 요청이 있을 경우 정합성에 문제가 발생할 가능성이 존재했다. 따라서 정렬할 행들에 모두 Lock을 걸어야 했는데, Lock이 어떤식으로 동작하는지 제대로 몰라 찾아보게 되었다.
Lock
데이터베이스에서 특정 리소스에 대한 접근을 막아 동시성을 제어하기 위한 기능.
Lock의 종류
- 공유 Lock (Shared Lock, Read Lock, S-Lock)
- 배타 Lock (Exclusive Lock, Write Lock, X-Lock)
- 어드바이저리 락 (Advisory Lock)
- ~~~
여기서 이야기한 공유Lock과 배타Lock은 데이터베이스가 거는 잠금의 종류를 이야기 하는 것이고, 흔히 이야기 하는 낙관락, 비관락은 더 어플리케이션 관점으로 해당 충돌을 어떻게 다룰지를 논한다.
공유 Lock
해당 데이터를 여러 트랜잭션이 동시에 읽을 수 있게 허용하는 락.
- 다른 트랜잭션도 해당 리소스를 읽는 것이 가능하다.
- 단, 해당 리소스에 대한 쓰기 작업은 불가능하다.
- 따라서 여러 트랜잭션이 동시에 읽을 수는 있지만,이후 들어온 트랜잭션은 수정이 불가능하다.
배타 Lock
특정 트랜잭션이 데이터를 독점적으로 쓰기 작업을 위해 잡고 있는 락.
- 이미 베타 Lock이 걸려있다면 다른 트랜잭션은 공유,배타 Lock 둘 다 걸지 못함.
어드바이저리 락 (Advisory Lock)
- 애플리케이션이 임의의 키로 직접 잡는 사용자 정의 락
- 비즈니스 규칙을 기준으로 키 값을 설정하여 락을 설정
Lock이 동작하는 방식
Lock은 이름처럼 시스템이나 데이터 자체를 물리적으로 잠그는 것이 아니라,
데이터베이스 내부의 Lock Manager라는 것이 어떤 트랜잭션이 어떤 리소스(테이블, 행 등)에 대해 어떤 락을 보유하고 있는지와 대기 중인 트랜잭션들을 내부 자료구조로 관리하는 메커니즘이다.
이후 동일한 리소스에 대한 요청이 들어오면 Lock Manager가 충돌 여부에 따라 트랜잭션을 즉시 실행하거나 대기시켜 작업의 순서를 제어한다.
Lock Manager
LockManager는 여러 트랜잭션이 동시에 같은 리소스에 접근할 때 누가 어떤 락을 언제까지 가질 수 있을지, 언제 가져갈 수 있을지 중앙에서 조율하는 관리자라고 생각하면 된다. 이때 메모리에 락에 대한 정보를 기록하고 이 정보를 확인하면서 접근할 수 있는지를 판단한다. 결국 락이란 LockManager가 내부의 메모리를 참고하여 트랜잭션의 허용, 대기, 해제를 결정하는 것이다.
내부 구조
- 락테이블
- 대기 큐(Wait Queue)
- 규칙테이블
- 등등
동작과정
락을 획득하고 관리하는 과정을 내부구조와 연동해서 이야기 해보면
Lock 요청과 Lock 상태 관리
- 트랜잭션이 SELECT ... FOR UPDATE, UPDATE 와 같은 명령에 의해 시작되면 Lock Manager는 해당 리소스에 적절한 Lock 모드를 자동으로 설정하게 된다.
Lock 모드와 호환성
- 이때 또 다른 Lock 요청이 들어오게 된다면 Lock 요청의 조합의 조건에 따라 허용할 지 대기할 지를 결정한다.
대기와 데드락
- 이때 특정 리소스에 대해 할당된 Lock과 충돌되는 Lock을 요청하게 된다면, 해당 트랜잭션의 세션(디비의 프로세스)은 Lock을 얻을때 까지 대기(wait) 상태가 된다.
잠금의 수명과 해제
- 테이블 Lock과 대부분의 행 Lock은 트랜잭션 단위로 유지된다.
Lock이 해제되면, 같은 리소스에 대해 대기하던 다른 세션이 이제 해당 리소스에 대한 Lock을 얻을 수 있게 되고, 이 과정 역시 내부 함수로 인해 자동으로 처리된다.
Lock 시스템의 문제점
상황 예시
- 락 기반 시스템을 떠올려 보면
- 트랜잭션 T1이 어떤 행을 수정하기 위해 배타 락을 잡고 있다.
- 같은 행을 읽고 싶은 트랜잭션 T2는 공유 락을 잡으려다 배타락과 충돌해, T1이 끝날 때까지 기다려야 한다.
왜 문제인가?
- 읽기 요청이 많은 서비스에서는 “조금만 늦어도 되는 단순 조회”까지, 쓰기 때문에 줄을 서게 된다.
이렇게 단순히 읽는 작업도 베타락에 의해 처리가 늦어지게 된다. 이런 문제를 해결하기 Postgresql은 새로운 아키텍쳐를 도입하게 된다.
MVCC (Multi Version Concurrency Control)
기존의 락 기반 동시성 제어가 가진 문제점은 쓰기가 읽기까지 막는 구조라는 점이었다. 이 문제를 해결하기 위해 PostgreSQL은 MVCC(Multi Version Concurrency Control, 다중 버전 동시성 제어) 라는 아키텍처를 채택했다. MVCC의 핵심 개념은 다음과 같다. “같은 데이터에 대해 여러 버전을 유지하고, 각 트랜잭션은 자기 시점에 맞는 버전만 본다.” 좀 더 자세하게 동작과정을 살펴보자.
동작과정
1. 트랜잭션 시작과 스냅샷 잡기
- 트랜잭션이나 개별 SQL 문이 실행될 때, Postgresql은 해당 시점의 데이터베이스 상태를 하나의 스냅샷으로 본다.
- 이 스냅샷은 “그 시점까지 커밋된 트랜잭션이 반영된 상태”를 의미하며, 이후 동시에 다른 트랜잭션이 데이터를 바꿔도, 내 문장은 이 스냅샷을 기준으로 일관된 데이터를 본다.
2. 읽기(SELECT) 시 동작
- SELECT는 실제 테이블을 보지 않고, 자신이 가진 스냅샷 기준으로 판단해서 결과를 만든다.
- 이 방식 덕분에, “읽기용으로 잡는 락과 쓰기용으로 잡는 락이 서로 충돌하지 않아서, 읽기가 쓰기를 막지 않고 쓰기가 읽기를 막지 않는다”는 보장이 생긴다.
3. 쓰기(INSERT/UPDATE/DELETE) 시 동작
- UPDATE나 DELETE 역시도 기존 행을 덮어쓰는 것이 아니라, 새로운 버전을 만들거나 행을 논리적으로 제거하는 식으로 “여러 버전”을 유지한다는 점이 MVCC의 핵심이다.
- 이때도 다른 트랜잭션의 SELECT는 자신이 가진 스냅샷 기준으로 예전 버전을 계속 볼 수 있으므로, 동시에 쓰기가 일어나도 읽기가 깨지지 않는다.
4. 버전 관리와 가시성
MVCC에서는 모든 레코드가 내부적으로 다음과 같은 정보를 가진다.
PostgreSQL은 이 정보를 바탕으로,
를 판단한다.
MVCC의 장점
MVCC는 다음과 같은 이점을 가진다.
- 읽기 성능이 크게 향상됨
- 읽기-쓰기 간 락 경합이 거의 사라짐
- 데이터베이스 접근 경합 감소
- 데드락 발생 빈도 감소
즉, 읽기 요청이 많은 서비스에 매우 유리한 구조다.
MVCC의 단점과 PostgreSQL의 해결 방식
MVCC는 장점만 있는 구조는 아니다.
데이터베이스 크기 증가
- UPDATE / DELETE가 많을수록
- 오래된 레코드 버전이 계속 쌓인다
이를 방치하면 테이블과 인덱스가 불필요하게 커진다.
VACUUM: PostgreSQL의 해결 전략
PostgreSQL은 이 문제를 해결하기 위해
VACUUM 이라는 백그라운드 프로세스를 사용한다.
VACUUM의 역할은 다음과 같다.
- 더 이상 어떤 트랜잭션에서도 참조하지 않는 오래된 버전 제거
- 디스크 공간 회수
주의할 점은,
- VACUUM도 내부적으로 리소스를 사용하고
- 경우에 따라 짧은 잠금을 유발할 수 있지만
VACUUM을 꺼서는 안된다. VACUUM이 동작하지 않으면,
결국 PostgreSQL은 공간 부족으로 인해 정상 동작이 불가능한 상태에 빠진다.
Lock과 MVCC
Postrgresql은 기본적으로 읽기 작업에 MVCC로 동시성을 해결하고, 필요할 경우 테이블·행 수준 락과 어드바이저리 락을 추가로 제공한다”고 명시한다.
- 즉, 일반적인 경우에는 MVCC 스냅샷만으로 동시성이 해결되지만, 특정 충돌 지점을 애플리케이션이 직접 제어하고 싶을 때는 Lock을 명시적으로 사용하도록 한다.
이렇게 Posgtgresql은 동시성을 제어하며 작동을 하게 된다. 간단하게 기존 Lock 시스템과 MVCC의 충돌 시점을 테이블로 비교하며 마무리 하겠다.
기존 Lock 구조
| 현재 작업 ↓ / 동시 요청 → | 읽기(Read) | 쓰기(Write) |
| 읽기(Read) | O | X |
| 쓰기(Write) | X | X |
MVCC
| 현재 작업 ↓ / 동시 요청 → | 읽기(Read) | 쓰기(Write) |
| 읽기(Read) | O | O |
| 쓰기(Write) | O | X |
출처
https://www.postgresql.org/docs/current/index.html
https://15445.courses.cs.cmu.edu/fall2022/notes/16-twophaselocking.pdf https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/What-is-MVCC-How-does-Multiversion-Concurrencty-Control-work