1편 · 약 10분
PostgreSQL 아키텍처 개요
PostgreSQL은 왜 다르게 느껴지는가
MySQL을 주로 다루던 DBA가 PostgreSQL을 처음 보면 낯선 점이 있다. 테이블 이름은 소문자로만 다뤄지고, UPDATE 한 번에 테이블이 부풀어 오르고, 튜닝 파라미터도 다르다. 이 낯섦의 대부분은 PostgreSQL의 프로세스 구조와 메모리 모델에서 비롯된다. 아키텍처를 먼저 잡아두면 이후 내용이 훨씬 쉽게 연결된다.
클라이언트-서버 모델과 프로세스 방식
PostgreSQL은 멀티프로세스 구조다. MySQL이 커넥션마다 스레드를 생성하는 것과 달리, PostgreSQL은 각 커넥션에 독립적인 OS 프로세스(backend)를 포크한다. 프로세스 간 통신은 공유 메모리(Shared Memory)로 이뤄진다.
이 구조는 장단점이 뚜렷하다.
| 항목 | PostgreSQL (프로세스) | MySQL (스레드) |
|---|---|---|
| 격리성 | 프로세스 크래시 시 다른 커넥션에 영향 없음 | 스레드 충돌이 전체 프로세스에 영향 |
| 메모리 | 커넥션당 work_mem 분리 | 전역 버퍼 공유, 커넥션당 오버헤드 작음 |
| 대규모 커넥션 | 수천 커넥션 시 포크 비용·메모리 문제 → pgBouncer 필요 | 상대적으로 넓은 커넥션 수 지원 |
PostgreSQL에서 수천 개의 동시 커넥션을 열면 각각 ~5–10 MB 메모리를 차지하므로, 프로덕션에서는 PgBouncer 같은 커넥션 풀러를 앞에 두는 것이 기본이다.
핵심 프로세스 구조
postmaster (PID 1)
↓ 포크
backend
커넥션 1 backend
커넥션 2 backend
커넥션 N WAL writer checkpointer bgwriter autovacuum
launcher stats collector archiver
커넥션 1 backend
커넥션 2 backend
커넥션 N WAL writer checkpointer bgwriter autovacuum
launcher stats collector archiver
- postmaster: 인스턴스 시작 시 단 하나 실행. 공유 메모리 할당, 설정 파일 로드, 나머지 모든 프로세스를 포크한다. 클라이언트 연결 요청이 들어오면 새 backend를 포크해 위임한다.
- backend: 클라이언트 커넥션 1개에 대응하는 프로세스. SQL 파싱 → 플래너 → 실행기 → 결과 반환을 담당한다. 커넥션이 끊기면 프로세스도 종료된다.
- WAL writer: 공유 메모리의 WAL 버퍼를
pg_wal/파일로 주기적으로 플러시한다. backend가 직접 WAL을 디스크에 쓰는 부담을 줄인다. - checkpointer:
checkpoint_timeout(기본 5분) 또는max_wal_size도달 시 dirty page 전체를 디스크에 플러시하고 WAL에 체크포인트 레코드를 기록한다. 크래시 복구 시 이 지점부터 재생한다. - bgwriter (Background Writer): 체크포인트 사이에 shared_buffers의 dirty page를 조금씩 미리 디스크에 내려 체크포인트 I/O 폭발을 완화한다.
- autovacuum launcher: MVCC 구조상 쌓이는 dead tuple을 청소하는 autovacuum worker를 필요한 테이블에 주기적으로 띄운다.
메모리 구조
PostgreSQL의 메모리는 공유 메모리와 로컬 메모리 두 계층으로 나뉜다.
공유 메모리 (전체 프로세스 공유)
shared_buffers
데이터 페이지 캐시 WAL buffers
커밋 전 WAL 임시 저장 Commit Log (pg_xact)
트랜잭션 상태 비트맵
⇄
데이터 페이지 캐시 WAL buffers
커밋 전 WAL 임시 저장 Commit Log (pg_xact)
트랜잭션 상태 비트맵
로컬 메모리 (backend 별)
work_mem
정렬·해시조인 버퍼 maintenance_work_mem
VACUUM·인덱스 빌드 temp buffers
임시 테이블
정렬·해시조인 버퍼 maintenance_work_mem
VACUUM·인덱스 빌드 temp buffers
임시 테이블
| 파라미터 | 역할 | 주의사항 |
|---|---|---|
shared_buffers | 공유 데이터 페이지 캐시. RAM의 25 % 가 기본 출발점 | 너무 크면 OS 페이지 캐시와 이중 캐싱 발생 |
work_mem | 커넥션당 정렬·해시조인에 사용하는 로컬 메모리 | 커넥션 수 × 복잡한 쿼리 병렬도 × work_mem = 실제 소비량 |
maintenance_work_mem | VACUUM, 인덱스 빌드에 사용. 크게 잡으면 VACUUM이 빨라짐 | 인덱스 병렬 빌드 시 worker 수만큼 곱해진다 |
wal_buffers | WAL을 디스크에 쓰기 전 임시 저장 버퍼. 기본 1/32 of shared_buffers | 고쓰기 워크로드에서는 64 MB 정도로 올리면 효과적 |
디스크 레이아웃 (PGDATA)
PostgreSQL의 데이터 디렉터리($PGDATA)에는 다음이 있다.
$PGDATA/
├── base/ ← 데이터베이스별 테이블·인덱스 파일 (OID별 서브디렉터리)
├── pg_wal/ ← Write-Ahead Log 파일 (기본 16 MB 세그먼트)
├── pg_xact/ ← 트랜잭션 커밋 상태 (Commit Log, CLOG)
├── pg_tblspc/ ← 심볼릭 링크로 타블스페이스 연결
├── global/ ← 클러스터 전체 공유 시스템 카탈로그
├── postgresql.conf
└── pg_hba.confbase/ 안에서 테이블은 OID 기반의 숫자 파일명으로 저장된다. 파일 크기가 1 GB를 넘으면 .1, .2 접미사로 자동 분할된다.
쿼리 실행 흐름
백엔드 프로세스가 쿼리를 받으면 다음 순서로 처리한다.
- 파서 — SQL 문자열 → 파스 트리
- 분석기(Analyzer) — 테이블·컬럼 존재 확인, 타입 해석
- 리라이터(Rewriter) — 뷰·룰을 실제 쿼리로 치환
- 플래너/옵티마이저 — 통계 정보(
pg_statistic)를 참조해 최저 비용 실행 계획 선택 - 실행기(Executor) — 계획에 따라 실제로 페이지를 읽고 결과 반환
EXPLAIN ANALYZE는 5단계 실행기가 실제로 어느 노드에서 얼마나 걸렸는지 보여준다. MySQL의 EXPLAIN FORMAT=JSON에 해당한다.
DBA 체크리스트
ps aux | grep postgres로 현재 프로세스 상태를 확인한다.idle in transaction이 오래 머물면 락 경합의 원인이 된다.pg_stat_bgwriter뷰로 bgwriter와 checkpointer 활동을 모니터링한다.- 커넥션 수가 100 이상이라면 PgBouncer(트랜잭션 풀링) 도입을 검토한다.
shared_buffers조정 후에는 반드시 인스턴스 재시작이 필요하다.
References
- https://www.postgresql.org/docs/current/tutorial-arch.html
- https://www.postgresql.org/docs/current/runtime-config-resource.html
- https://www.enterprisedb.com/blog/postgres-internals-deep-dive-process-architecture
- https://ahmed-n-abdeltwab.github.io/blog/2025/06/17/postgresql-architecture-processes.html
- https://www.postgresql.fastware.com/blog/lets-get-back-to-basics-postgresql-memory-components