목차
1. 데이터 타입 및 유연성(JSONB)
2. 동시성 제어와 성능(MVCC, VACUUM)
3. 성능 최적화(인덱스)
4. 아키텍처 확장성 및 고급 기능 (테이블 상속, FDW, PostGIS)
json
타입은 JSON 데이터를 입력한 그대로 텍스트 형태로 저장된다. 데이터의 유효성만 검사하고, 내부 구조에 대한 별도의 처리를 하지 않는다.
jsonb
타입은 JSON 데이터를 이진(binary) 형식으로 분해하여 저장한다. 데이터를 입력하는 순간 바이너리 형태로 변환하고, 이 때 불필요한 공백을 제거하고 키-값 쌍의 순서를 재정렬한다.
JSONB
내부의 특정 키나 값에 대한 검색 속도를 혁신적으로 높일 수 있다.json
타입보다 쓰기(insertion) 작업이 상대적으로 느리다.특징 | JSON | JSONB |
---|---|---|
저장 방식 | 입력된 텍스트 그대로 저장 | 이진(binary) 형태로 분해하여 저장 |
저장 공간 | 원본 텍스트 크기 | 텍스트보다 작을 수 있음(공백, 중복키 제거) |
성능(읽기) | 느림(매번 파싱 필요) | 매우 빠름(파싱 없이 바로 접근) |
성능(쓰기) | 빠름 | 상대적으로 느림(변환 과정 필요) |
인덱싱 | 인덱스 지원하지 않음 | GIN 인덱스 지원 |
데이터 순서 | 유지됨 | 유지되지 않음 |
중복키 | 허용됨 | 허용되지 않음 |
결론적으로, 데이터가 자주 변경되거나 검색이 빈번하게 발생하는 어플리케이션에서는 JSONB
를 사용하는 것이 훨씬 더 효율적이다. 데이터를 한 번 저장하고 여러번 읽는 웹 서비스나 동적 스키마가 필요한 경우(예: 제품의 다양한 속성, 블로그의 콘텐츠 블록)에 JSONB
는 필수적인 선택이다. 반면, 단순히 로그나 설정 파일을 저장하는 등 데이터 조작이나 검색이 거의 없는 경우에는 JSON
을 사용하는 것이 적절하다.
여러 트랜잭션이 동시에 데이터베이스에 접근해도 데이터의 일관성을 유지하는 기술이다. 데이터를 직접 덮어쓰지 않고 변경 사항마다 새로운 버전의 데이터를 만드는 방식이다. 데이터 변경시 UPDATE
나 DELETE
명령어가 기존 데이터를 직접 수정하거나 삭제하지 않는다.
새로운 데이터 버전 생성 : 어떤 트랜잭션이 특정 행(row)을 UPDATE
하면, PostgreSQL은 기존 행을 삭제 처리하지 않고 그대로 둔다. 대신 변경된 내용을 담은 새로운 행을 생성한다.
버전 가시성 제어 : 각 트랜잭션은 고유한 타임스탬프(snapshot)를 가진다. 트랜잭션은 자신이 시작된 시점의 스냅샷을 기준으로 데이터를 읽는다.
UPDATE
는 기존 버전의 행을 "무효화" 처리하고, 새로운 버전의 행을 만든다. DELETE
는 단순히 기존 행을 무효화 처리합니다.죽은 튜플(Dead Tuple) : UPDATE
나 DELETE
로 인해 무효화된 기존 데이터 행을 "죽은 튜플"이라고 부른다. 이 죽은 튜플들은 당장 삭제되지 않고, 다른 트랜잭션이 모두 작업을 마친 후에 VACUUM 프로세스에 의해 정리된다.
읽기 작업의 차단 없음(Reader-Writer Isolation) : 데이터를 읽는 트랜잭션(Reader)이 데이터를 쓰는 트랜잭션(Writer)을 기다리지 않는다. 서로 영향을 주지 않으므로, 읽기 작업이 쓰기 작업에 의해 차단되지 않아 동시성이 크게 향상된다.
잠금(Locking) 오버헤드 감소 : 읽기 작업에 잠금이 필요하지 않아 잠금으로 인한 성능 저하가 줄어든다.
스냅샷 일관성 : 각 트랜잭션이 자신이 시작된 시점의 데이터 상태를 보장받으므로, Repeatable Read(반복 가능한 읽기) 같은 격리 수준을 쉽게 구현할 수 있다.
스토리지 오버헤드 : UPDATE
마다 새로운 행이 생성되므로, 데이터베이스 크기가 증가한다.
VACUUM 프로세스 필요 : 주기적으로 죽은 튜플을 정리하는 VACUUM
이라는 별도의 작업이 필요하다. VACUUM
작업이 제대로 이루어지지 않으면 불필요한 데이터가 쌓여 성능 저하를 초래할 수 있다.
READ COMMITTED (기본값) : 각 쿼리가 실행되는 시점의 최신 커밋된 데이터를 읽는다. 즉, 한 트랜잭션 내에서도 각 SELECT
문마다 다른 데이터를 볼 수 있다.
REPEATABLE READ : 트랜잭션이 시작된 시점의 스냅샷을 기준으로 데이터를 읽는다. 트랜잭션이 종료될 때까지 동일한 SELECT
문을 실행하면 항상 같은 결과를 반환한다.
VACUUM
은 데이터베이스의 성능을 유지하고 공간을 재확보하는 데 필수적인 관리 작업이다. MVCC(다중 버전 동시성 제어) 모델 때문에 발생하는 문제를 해결하는 핵심 메커니즘이다.
PostgreSQL의 MVCC는 데이터를 업데이트하거나 삭제할 때 기존 데이터를 즉시 제거하지 않고, 새로운 버전의 데이터를 생성하거나 기존 데이터에 '삭제' 표시를 한다. 이렇게 무효화된 기존 데이터 행을 죽은 튜플(dead tuple) 이라고 한다. 이 죽은 튜플들은 디스크 공간을 계속 차지하고, 인덱스를 비대하게 만들어 데이터베이스 성능을 저하시킨다. VACUUM
은 이러한 죽은 튜플을 찾아 정리하고, 해당 공간을 재사용 가능하도록 만드는 역할을 한다.
공간 재확보 : VACUUM
의 주된 목적은 죽은 튜플이 차지하는 공간을 회수하는 것이다. VACUUM
은 테이블을 스캔하며 더 이상 어떤 트랜잭션도 볼 수 없는 죽은 튜플을 식별한다. 이렇게 식별된 공간은 '프리 스페이스(free space)'로 표시되어 새로운 데이터를 삽입하거나 업데이트할 때 재사용된다.
트랜잭션 ID 순환 문제 방지 : PostgreSQL의 모든 트랜잭션은 32비트 정수형인 트랜잭션 ID를 가진다. 트랜잭션 ID는 계속 증가하며 언젠가는 최댓값에 도달하여 0부터 다시 시작하게 된다. 이를 트랜잭션 ID 순환(transaction ID wraparound) 이라고 한다. 만약 VACUUM
이 주기적으로 실행되지 않으면, 시스템은 트랜잭션 ID의 최댓값에 도달했을 때 모든 트랜잭션을 중단시키는 '트랜잭션 ID 순환 정지(shutdown)' 상태에 빠지게 된다. VACUUM
은 각 행의 트랜잭션 ID를 '동결(freeze)'시켜 이러한 문제를 예방한다.
VACUUM : 가장 기본적인 형태로, 테이블을 스캔하여 죽은 튜플이 차지하는 공간을 재확보한다. 하지만 이 공간을 운영체제에 반환하지 않고, 데이터베이스 내부에서만 재사용 가능하도록 만든다. 이 작업은 테이블에 대한 공유 잠금(shared lock)을 획득하지만, 다른 읽기/쓰기 작업과 동시에 실행될 수 있어 대부분의 경우 블로킹이 발생하지 않는다.
VACUUM FULL : 테이블의 모든 내용을 새로운 파일로 재작성하는, 더 강력한 버전이다. 이 과정에서 죽은 튜플을 완전히 제거하여 디스크 공간을 운영체제에 반환한다. 매우 무거운 작업으로, 실행되는 동안 해당 테이블에 대한 배타 잠금(exclusive lock) 을 획득한다. 따라서 테이블에 대한 모든 다른 작업(읽기/쓰기)을 차단하므로, 시스템 성능에 심각한 영향을 미칠 수 있다. 일반적으로 꼭 필요한 경우가 아니면 사용하지 않는다.
인덱스는 데이터베이스 테이블에서 원하는 데이터를 더 빠르고 효율적으로 찾기 위해 사용하는 자료 구조이다. 책의 목차나 찾아보기처럼, 인덱스가 없으면 모든 데이터를 처음부터 끝까지 스캔해야 하지만(순차 스캔), 인덱스가 있으면 특정 데이터를 빠르게 탐색할 수 있다. PostgreSQL은 다양한 용도에 맞는 여러 인덱스 타입을 제공한다.
B-tree: 가장 일반적이고 기본적으로 사용되는 인덱스 타입이다. 균형 트리(Balanced Tree) 구조를 사용하여 데이터가 항상 정렬된 상태로 유지된다. 등가 비교(=), 부등호 비교(>, <, >=, <=), BETWEEN, IN 등 대부분의 연산에 효율적이며, 텍스트, 숫자, 날짜 등 거의 모든 데이터 타입에 적용할 수 있다.
Hash: 데이터의 해시값을 사용하여 인덱스를 구성한다. 오직 등가 비교(=) 에만 특화되어 있으며, B-tree보다 성능이 약간 더 빠를 수 있다. 그러나 충돌 문제 및 부등호 연산에 사용할 수 없다는 단점 때문에 활용도가 제한적이다.
GIN (Generalized Inverted Index): 여러 키-값 쌍을 포함하는 복합 데이터 타입(예: 배열, JSONB)에 매우 효율적인 인덱스이다. 데이터를 역으로 매핑하는 구조를 사용해, 특정 배열 요소나 JSON 객체 내의 특정 키를 포함하는 모든 행을 빠르게 찾을 수 있다.
GiST (Generalized Search Tree): 복잡한 데이터 타입과 비전형적인 검색 연산(예: 공간 데이터, Full-Text Search)을 지원하는 다목적 인덱스이다. GiST 인덱스는 데이터를 계층적으로 분할하여, 특정 범위 내의 데이터를 찾는 공간 검색이나, 텍스트 문서 내의 단어를 찾는 전문 검색에 사용된다.
PostgreSQL의 테이블 상속은 객체 지향 프로그래밍의 상속 개념을 데이터베이스 테이블에 적용한 기능이다. 부모 테이블의 구조(컬럼, 제약 조건 등)를 상속받아 여러 개의 자식 테이블을 생성할 수 있다.
동작 방식: CREATE TABLE ... INHERITS parent_table
구문을 사용하여 자식 테이블을 만든다. 자식 테이블은 부모 테이블의 모든 컬럼을 가지며, 고유한 컬럼을 추가할 수도 있다.
활용: 특정 기준에 따라 데이터를 분할할 때 유용하다. 예를 들어, sales라는 부모 테이블 아래에 sales_2023, sales_2024와 같은 자식 테이블을 만들어 연도별 데이터를 분리할 수 있다. 이렇게 하면 sales 부모 테이블을 쿼리할 때 모든 자식 테이블의 데이터가 합쳐져서 조회되므로, 데이터 관리 및 쿼리가 훨씬 편리해진다.
PostGIS는 PostgreSQL을 강력한 지리 정보 시스템(GIS) 데이터베이스로 변모시키는 확장 모듈이다. PostgreSQL의 확장성 덕분에 가능한 기능이다.
기능
공간 데이터 타입: POINT, LINESTRING, POLYGON 등 다양한 공간 데이터 타입을 제공한다.
공간 함수: 두 지점 간의 거리 계산, 특정 영역 내에 있는 지점 찾기, 교차하는 선 찾기 등 복잡한 지리 공간 쿼리를 위한 수백 가지 함수를 제공한다.
공간 인덱스: GiST 인덱스를 활용하여 공간 데이터에 대한 검색 성능을 최적화한다.
FDW는 PostgreSQL 서버가 외부 데이터 소스에 접근하여 마치 로컬 테이블처럼 쿼리할 수 있게 해주는 기능이다. 분산된 데이터를 통합하여 하나의 데이터베이스에서 관리하는 데 유용하다.
동작 방식
CREATE EXTENSION
으로 FDW 모듈을 설치한다 (예: postgres_fdw).CREATE SERVER
로 외부 데이터베이스 연결 정보를 정의한다.CREATE USER MAPPING
으로 사용자 인증 정보를 매핑한다.CREATE FOREIGN TABLE
로 외부 테이블을 로컬에 '외부 테이블'로 정의한다.활용: 다른 PostgreSQL 서버뿐만 아니라 MySQL, MongoDB, Redis, 심지어 CSV 파일이나 웹 API까지 다양한 외부 데이터 소스를 연결할 수 있다. 이는 데이터 분석, 데이터 웨어하우징, 분산 데이터 통합 아키텍처에서 매우 강력한 도구이다.