백엔드 데이터 인프라 3편. PostgreSQL의 클라이언트·서버 아키텍처와 클러스터·데이터베이스·스키마·테이블 4단계 계층을 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 3편이에요. 2편 에서 PG(PostgreSQL 약칭)를 로컬에 띄웠으니, 이번 3편은 "우리가 띄운 그 PG가 어떻게 생긴 시스템인지" 큰 그림을 풀어 가요.
아키텍처가 헷갈리는 이유
처음 PG 접하면 — "서버? 데이터베이스? 클러스터? 스키마? 다 다른 거야?" 가 헷갈려요. 같은 단어 "데이터베이스" 가 운영체제 관점·SQL 관점·앱 관점에서 의미가 미묘하게 달라요.
이 글에서는 두 가지 그림으로 풀어 가요. 하나는 클라이언트·서버 그림 — 누가 누구에게 뭘 요청하나. 또 하나는 계층 그림 — 데이터가 어디에 어떻게 묶여 있나.
클라이언트·서버 그림
PG는 C/S 모델(클라이언트·서버 구조). 단순화하면:
[애플리케이션 / psql] → TCP/IP 5432 → [PostgreSQL 서버]
클라이언트 서버 프로세스
여기서 클라이언트는 SQL 쿼리를 보내는 누구든 — Spring 백엔드·psql·DBeaver·Python·다른 데이터베이스 다 포함이에요. 서버는 postgres 프로세스, 5432 포트를 listen 해요. 둘 사이는 PostgreSQL Frontend/Backend Protocol(TCP 위에서 동작하는 PG 전용 통신 규약)로 오가요.
프로세스 모델 — 멀티 프로세스
MySQL이 "스레드 기반" 이라면 PG는 프로세스 기반.
postgres (postmaster 프로세스 = 부모)
├── postgres: writer process
├── postgres: wal writer process
├── postgres: autovacuum launcher
├── postgres: stats collector
├── postgres: logical replication launcher
├── postgres: backend process (client 1) ← 클라이언트 1 접속
├── postgres: backend process (client 2) ← 클라이언트 2 접속
└── postgres: backend process (client N) ← 클라이언트 N 접속
클라이언트가 접속하면 postmaster(연결을 받아 자식 프로세스를 만드는 부모)가 "새 backend 프로세스" 를 fork(부모 프로세스를 복제해 자식 생성) 해요. 클라이언트와 backend는 1:1 로 묶이고, 보조 프로세스(writer·WAL·autovacuum 등)는 그 옆에서 항상 떠 있어요.
시사점: - 접속이 많으면 프로세스도 많아 (수백 ~ 수천 가능) - 프로세스 생성 비용이 "스레드보다 무거움" — 연결 풀(HikariCP·PgBouncer) 사용 거의 필수
계층 그림 — 데이터의 4단계
PG 안 데이터를 "가장 큰 단위 → 가장 작은 단위" 로 나누면 4단계.
1. Cluster = 한 PG 서버 인스턴스 전체 (port 5432 하나)
2. Database = 클러스터 안 격리된 데이터베이스 (psql 명령 \l 로 보이는 목록)
3. Schema = 데이터베이스 안 네임스페이스 (psql 명령 \dn)
4. Table·Index = 스키마 안 객체
같은 단어 "데이터베이스" 가 헷갈리는 이유 — 운영체제 관점에선 "서버 전체" 를 가리키기도 하고, SQL 관점에선 "클러스터 안 한 개" 를 가리키기도.
1단계 — Cluster
클러스터 = 한 PG 서버 인스턴스. 데이터 디렉토리(/var/lib/postgresql/data 또는 PGDATA) 한 개에 해당.
오해 주의 — "여러 서버 모은 거" 가 아님. PG에서 "클러스터" = "한 서버에서 관리하는 데이터베이스들의 묶음". (분산 시스템의 클러스터 개념과 다름.)
2단계 — Database
클러스터 안에 여러 개. 각 데이터베이스는 서로 격리 — 다른 DB의 테이블을 "SELECT * FROM other_db.users" 같이 직접 못 봄 (외부 연결 필요).
-- 클러스터 안 DB 목록
\l
-- DB 만들기
CREATE DATABASE myappdb;
-- 접속 변경
\c myappdb
기본 = postgres 라는 관리용 DB가 있어요. 새 프로젝트 = 새 DB 만들기.
3단계 — Schema
DB 안 네임스페이스. "테이블 이름 충돌 방지" + "논리적 그룹화".
-- 스키마 목록
\dn
-- 기본 = public 스키마
SELECT * FROM users; -- public.users 와 같음
-- 별도 스키마 만들기
CREATE SCHEMA app;
CREATE TABLE app.users (...);
-- 스키마 명시
SELECT * FROM app.users;
기본 = public 스키마. 그래서 처음엔 "스키마가 있는지도 모르고" 사용 가능. 큰 시스템 = 스키마로 그룹화 (예: app·audit·reporting).
4단계 — Table·Index·View·Sequence
스키마 안에 진짜 객체들. 테이블 이 가장 많이 다루는 객체.
-- public 스키마 테이블 목록
\dt
-- 또는 다른 스키마
\dt app.*
테이블 외 — 인덱스(34~36편)·뷰(16편)·시퀀스·트리거·함수·타입 등 다양한 객체 존재.
데이터가 어디에 저장되나
클러스터 = 호스트의 한 디렉토리 (PGDATA). 그 안에:
/var/lib/postgresql/data/
├── base/ # 데이터베이스별 디렉토리 (숫자 OID)
│ ├── 1/ # template1
│ ├── 13520/ # postgres
│ └── 16384/ # myappdb
├── global/ # 클러스터 전체 공유 (사용자·DB 카탈로그)
├── pg_wal/ # Write-Ahead Log (트랜잭션 로그)
├── pg_stat/ # 통계
├── postgresql.conf # 메인 설정
├── pg_hba.conf # 인증 설정
└── ...
여기서 디렉토리 이름에 붙은 OID(Object Identifier, PG 내부 객체 식별 번호)가 곧 데이터베이스 한 개를 가리켜요. pg_wal 이 가장 중요 — 모든 변경이 먼저 여기 적힘 (Write-Ahead Logging, 데이터 파일보다 로그를 먼저 기록하는 방식). 6편 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어) 트랜잭션 격리와 함께 깊이 풀어요.
인증·연결 흐름
1. 클라이언트가 5432로 TCP 연결
2. postmaster 가 받음
3. pg_hba.conf 룰로 인증 검증
4. 통과 시 새 backend process fork
5. 클라이언트 ↔ backend 1:1 통신 시작
6. 클라이언트 종료 시 backend 도 종료
pg_hba.conf 가 "누가 어디서 어떤 방법으로 접속할 수 있나" 정의. 운영 환경 보안의 첫 관문.
Shared Buffer·캐시
PG는 자체 메모리 영역(Shared Buffer, PG 전용 페이지 캐시 영역)에 데이터·인덱스 페이지 캐시. 일반적으로 호스트 메모리의 25% 정도 할당. 보조로 OS 페이지 캐시도 사용.
# postgresql.conf
shared_buffers = 2GB # 8GB 호스트면 2GB 추천
work_mem = 16MB # 정렬·해시당
effective_cache_size = 6GB # OS 캐시 포함 가용 메모리 추정치
Spring Boot 관점 — 우리가 다루는 것
자바 백엔드 입문 44편 JPA(Java Persistence API, 자바 객체-DB 매핑 표준) 에서 본 application.yml 의 jdbc URL — jdbc:postgresql://localhost:5432/postgres — 가 정확히 어디를 가리키나:
localhost:5432= 클러스터 (1단계)postgres= 데이터베이스 (2단계)- 명시 안 했지만 =
public스키마 기본 (3단계) - 코드에서 다루는
@Entity= 테이블 (4단계)
이 4단계가 머릿속에 박혀 있으면 — 운영 환경 트러블슈팅·복구·마이그레이션 시 매번 "내가 지금 어느 단계 다루고 있나" 명확해져요.
함정 5가지
(1) "DB"라는 단어의 모호함
"PG라는 DB" (= 클러스터·서버) vs "내 DB 마이그레이션" (= 클러스터 안 한 개 DB) 헷갈리지 말 것. 문맥에 따라 의미 다름.
(2) 다른 DB 직접 조회 불가
SELECT * FROM otherdb.public.users; -- ❌ 같은 클러스터라도 다른 DB는 직접 조회 X
해결은 postgres_fdw(Foreign Data Wrapper, 외부 데이터 연결 확장) 를 쓰거나 별도 연결을 만들어야 해요.
(3) public 스키마의 함정
기본 스키마라 너무 자유로워 — 큰 시스템에선 "모든 테이블이 public에 박혀 충돌". 일찍 스키마로 분리하는 게 운영 단계에서 편함.
(4) 프로세스 수 폭발
연결 풀 없이 매 요청마다 새 연결을 만들면 — 수백 ~ 수천 프로세스 → 메모리·CPU 부담. HikariCP (자바 백엔드 입문 41편 JDBC(Java Database Connectivity, 자바 DB 연결 API) 참고) + PgBouncer (앞단 풀) 가 운영 표준.
(5) pg_hba.conf 무시
기본값으로 두면 — 외부 접속 막혀 있거나, "모든 호스트에 trust" 같이 위험. 운영 = IP 화이트리스트 + scram-sha-256(PG가 권장하는 챌린지-응답 인증 방식) 인증.
Cluster → Database → Schema → Table. 같은 단어 "데이터베이스" 가 문맥에 따라 클러스터 또는 한 개 DB 가리킬 수 있어요. 머릿속에 4단계를 그려 두면 운영 단계 트러블슈팅이 한결 깔끔.
한 줄 정리 — PG = C/S 모델 + 멀티 프로세스. Cluster → Database → Schema → Table 4단계 계층. shared_buffers·pg_wal·pg_hba.conf 세 가지가 핵심 운영 영역. HikariCP·PgBouncer 가 연결 풀 표준.
시험 직전 한 번 더 — PostgreSQL 아키텍처 입문자가 매번 헷갈리는 것
- C/S 모델 = 클라이언트가 SQL 보내고 서버가 처리
- TCP 5432 = 기본 포트
- 프로세스 모델 = 멀티 프로세스 (MySQL은 스레드)
- postmaster = 부모 프로세스, backend = 자식 (클라이언트 1:1)
- 보조 프로세스 = writer·WAL writer·autovacuum·stats collector
- 4단계 계층 = Cluster → Database → Schema → Table
- Cluster = 한 PG 서버 인스턴스 (PGDATA 한 개)
- Database = 클러스터 안 격리된 DB
- Schema = DB 안 네임스페이스 (기본 = public)
- 다른 DB 직접 조회 X — postgres_fdw 필요
- 데이터 저장 =
/var/lib/postgresql/data(PGDATA) - pg_wal = Write-Ahead Log (트랜잭션 변경)
- postgresql.conf = 메인 설정 (43편)
- pg_hba.conf = 인증 설정 (보안의 첫 관문)
- Shared Buffer = PG 자체 메모리 캐시 (호스트 메모리 25% 권장)
- 연결 풀 = HikariCP (앱) + PgBouncer (서버 앞단)
- public 스키마의 함정 = 큰 시스템에선 일찍 분리
- 운영 인증 = scram-sha-256 + IP 화이트리스트
- jdbc URL =
jdbc:postgresql://host:port/dbname(3단계까지 명시) - Spring
@Entity= 테이블 (4단계) - shared_buffers = 호스트 메모리 25%
- work_mem = 정렬·해시당 메모리 (16MB ~)
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
다음 글: