PostgreSQL의 프로세스와 메모리 구조
PostgreSQL의 멀티 프로세스 아키텍처와 메모리 구조를 Postmaster, Backend Process, Shared Buffer까지 정리합니다.
이전 글에서는 PostgreSQL이 데이터를 어떻게 저장하는지 — Cluster, Database, Page의 구조를 살펴보았다. 이번에는 한 단계 올라가서, PostgreSQL 서버가 어떤 프로세스로 구성되고, 메모리를 어떻게 나눠 쓰는지 정리해보았다.
1. 멀티 프로세스 아키텍처
PostgreSQL의 가장 큰 특징 중 하나는 멀티 스레드가 아닌 멀티 프로세스 모델이라는 점이다. MySQL이나 Oracle이 하나의 프로세스 안에서 스레드를 나눠 쓰는 반면, PostgreSQL은 클라이언트 연결마다 독립된 프로세스를 생성한다.

왜 이런 설계를 택했을까? PostgreSQL의 전신인 POSTGRES가 1986년에 탄생했을 당시, 대부분의 OS에서 스레드 지원이 미약했다. 프로세스 기반 설계가 당시로서는 자연스러운 선택이었고, 이 구조가 지금까지 이어져 오고 있다.
| 특성 | 멀티 프로세스 (PostgreSQL) | 멀티 스레드 (MySQL 등) |
|---|---|---|
| 안정성 | 하나의 Backend가 죽어도 다른 연결에 영향 없음 | 하나의 스레드 크래시가 전체 프로세스를 위협 |
| 격리성 | 프로세스 간 메모리 격리가 OS 수준에서 보장 | 공유 메모리 접근에 더 세심한 동기화 필요 |
| 디버깅 | ps, strace 등으로 개별 프로세스 추적 가능 | 스레드 간 문제 추적이 상대적으로 어려움 |
| 메모리 효율 | 프로세스당 오버헤드 존재 | 스레드가 메모리를 공유하므로 더 효율적 |
| 컨텍스트 스위칭 | 프로세스 전환 비용이 상대적으로 큼 | 스레드 전환이 더 가벼움 |
이 트레이드오프 때문에 PostgreSQL에서는 커넥션 풀링(PgBouncer, Supavisor 등)이 거의 필수다. 연결마다 프로세스가 생기니, 커넥션 수가 수백 개를 넘어가면 메모리와 컨텍스트 스위칭 비용이 급격히 증가한다.
Postmaster
PostgreSQL 서버를 시작하면 가장 먼저 Postmaster 프로세스가 뜬다. 이 프로세스가 하는 일은 크게 세 가지다.
- 클라이언트 연결 수신 — TCP 포트(기본 5432)를 열고 연결 요청을 대기
- Backend 프로세스 생성 — 연결이 들어오면
fork()로 새 Backend 프로세스를 만들어 할당 - Background 프로세스 관리 — 서버 시작 시 필요한 백그라운드 프로세스들을 함께 기동
Postmaster 자체는 쿼리를 실행하지 않는다. 일을 직접 하진 않지만 모든 것이 이 프로세스에서 시작된다.
Backend Process
클라이언트가 연결되면 Postmaster가 fork()로 Backend Process를 하나 만든다. 이 프로세스가 해당 클라이언트의 모든 쿼리를 처리한다. SQL 파싱, 실행 계획 수립, 데이터 읽기/쓰기까지 전부 이 프로세스 안에서 일어난다.
하나의 Backend Process가 하나의 클라이언트만 전담한다는 것이 핵심이다. 동시에 100명이 접속하면 100개의 Backend Process가 뜬다. 이것이 커넥션 풀링이 중요한 이유다.
-- 현재 활성 Backend Process 수 확인
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';Background Process
Postmaster는 서버 시작 시 여러 Background Process도 함께 띄운다. 이들은 클라이언트 요청과 무관하게 서버를 유지하는 역할을 한다.
| 프로세스 | 역할 |
|---|---|
| Background Writer | Shared Buffer의 Dirty Page를 주기적으로 디스크에 기록 |
| Checkpointer | 체크포인트 실행 — 모든 Dirty Page를 디스크에 쓰고 WAL 위치 기록 |
| WAL Writer | WAL Buffer의 내용을 WAL 파일에 주기적으로 flush |
| WAL Summarizer | WAL 레코드를 요약해서 Incremental Backup에 활용 (v17+) |
| Autovacuum Launcher | Dead Tuple 정리와 통계 갱신을 위한 VACUUM 자동 실행 |
| Statistics Collector | 테이블/인덱스 접근 통계 수집 — Planner가 실행 계획 수립 시 활용 |
| Logging Collector | 서버 로그를 파일로 기록하고 로테이션 관리 |
| I/O Worker | 비동기 I/O 작업 처리 (v17+) |
| Archiver | WAL 파일을 아카이브 위치에 복사 — Point-in-Time Recovery에 사용 |
이 프로세스들은 ps 명령으로 직접 확인할 수 있다.
$ ps aux | grep postgres
postgres 1234 postmaster
postgres 1235 checkpointer
postgres 1236 background writer
postgres 1237 walwriter
postgres 1238 autovacuum launcher
postgres 1239 stats collector
postgres 1240 backend # 클라이언트 연결2. 메모리 아키텍처
PostgreSQL의 메모리는 크게 두 영역으로 나뉜다. 모든 프로세스가 함께 쓰는 Shared Memory와, Backend Process마다 독립적으로 가지는 Local Memory다.

Shared Memory
Shared Memory는 서버 시작 시 한 번 할당되며, 모든 Backend Process와 Background Process가 함께 접근한다.
Shared Buffer Pool
가장 크고 가장 중요한 영역이다. 디스크에서 읽어온 Page를 캐싱하는 곳으로, 모든 읽기/쓰기 작업은 이 버퍼를 거친다.
이전 글에서 테이블 데이터가 8KB Page 단위로 저장된다고 했다. SELECT를 실행하면 디스크의 Page가 먼저 Shared Buffer에 올라오고, Backend Process는 이 버퍼에서 데이터를 읽는다. INSERT나 UPDATE도 마찬가지로 Shared Buffer 위에서 수행된다.
-- 기본값은 128MB, 보통 전체 RAM의 25%를 권장
SHOW shared_buffers;
-- 128MBShared Buffer는 Clock Sweep 알고리즘으로 관리된다. 자주 접근되는 Page는 버퍼에 오래 남고, 오랫동안 사용되지 않은 Page는 새 Page가 필요할 때 밀려난다. LRU(Least Recently Used)와 비슷하지만, 구현이 더 가볍다.
여기서 핵심 개념이 Dirty Page다. Shared Buffer에서 수정된 Page는 아직 디스크에 반영되지 않은 상태인데, 이걸 Dirty Page라 부른다. 앞서 본 Background Writer와 Checkpointer가 이 Dirty Page를 주기적으로 디스크에 기록하는 역할을 맡는다.
WAL Buffer
WAL(Write-Ahead Log)은 데이터를 변경하기 전에 먼저 변경 내용을 로그로 기록하는 메커니즘이다. WAL Buffer는 이 로그를 임시로 보관하는 메모리 영역이다.
왜 데이터를 바로 디스크에 쓰지 않고 로그를 먼저 쓸까?
- 성능: 데이터 파일에 랜덤 쓰기를 하는 것보다 WAL 파일에 순차 쓰기를 하는 것이 훨씬 빠르다
- 안정성: 서버가 갑자기 죽어도 WAL을 재생(replay)하면 마지막 커밋 상태로 복구할 수 있다
-- 기본값은 -1 (shared_buffers의 1/32)
SHOW wal_buffers;
-- 4MBWAL Writer가 이 버퍼의 내용을 주기적으로 pg_wal/ 디렉토리의 파일에 flush한다. 커밋 시에는 즉시 flush가 일어난다.
Commit Log (CLOG)
모든 트랜잭션의 커밋 상태를 기록하는 영역이다. 각 트랜잭션은 네 가지 상태 중 하나를 가진다.
| 상태 | 의미 |
|---|---|
IN_PROGRESS | 아직 실행 중 |
COMMITTED | 커밋 완료 |
ABORTED | 롤백됨 |
SUB_COMMITTED | 서브트랜잭션 커밋 |
MVCC(Multi-Version Concurrency Control)에서 Tuple의 가시성을 판단할 때 이 정보를 참조한다. "이 Tuple을 만든 트랜잭션이 커밋됐는가?"를 확인하는 것이다.
Local Memory
Local Memory는 각 Backend Process가 독립적으로 할당받는 메모리다. 다른 프로세스와 공유되지 않으며, 해당 프로세스가 종료되면 함께 해제된다.
work_mem
정렬(ORDER BY)과 해시 조인 등의 작업에 사용되는 메모리다. 이 값보다 데이터가 크면 디스크에 임시 파일을 만들어 처리하는데, 당연히 훨씬 느려진다.
SHOW work_mem;
-- 4MB
-- 디스크로 넘어가는지 확인하려면
EXPLAIN ANALYZE SELECT * FROM large_table ORDER BY column1;
-- Sort Method: external merge Disk: 102400kB ← 디스크 사용 중!
-- Sort Method: quicksort Memory: 25kB ← 메모리 안에서 처리주의할 점은, work_mem이 쿼리당이 아니라 작업(operation)당 할당된다는 것이다. 하나의 쿼리 안에 정렬 2번, 해시 조인 1번이 있으면 work_mem × 3이 사용될 수 있다. 동시 접속자가 100명이면 최악의 경우 work_mem × 작업 수 × 100이 되므로, 무작정 크게 잡으면 메모리가 부족해진다.
maintenance_work_mem
VACUUM, CREATE INDEX, ALTER TABLE ADD FOREIGN KEY 같은 유지보수 작업 전용 메모리다. 이런 작업은 동시에 여러 개 실행되는 경우가 드물기 때문에 work_mem보다 넉넉하게 설정해도 괜찮다.
SHOW maintenance_work_mem;
-- 64MB
-- 큰 테이블에 인덱스를 생성할 때 이 값을 높이면 속도가 빨라진다
SET maintenance_work_mem = '512MB';
CREATE INDEX CONCURRENTLY idx_large ON large_table(column1);temp_buffers
임시 테이블에 접근할 때 사용하는 버퍼다. Shared Buffer가 아닌 Local Memory에서 관리되므로 다른 세션과 격리된다.
SHOW temp_buffers;
-- 8MB임시 테이블을 자주 사용하는 세션에서만 의미가 있으며, 사용하지 않으면 할당되지 않는다.
이전 글이 "데이터가 어디에 있는가"였다면, 이번 글은 "그 데이터를 누가, 어떤 메모리 위에서 다루는가"에 대한 이야기였다. 이 구조를 알면 shared_buffers 설정이 왜 성능에 가장 큰 영향을 미치는지, 커넥션 풀링이 왜 필요한지가 자연스럽게 이해된다. 다음 글에서는 이 프로세스와 메모리가 실제 쿼리를 처리할 때 어떻게 협력하는지 살펴볼 예정이다.