본문 바로가기
IT/소프트웨어

🗄️ PostgreSQL 테이블 설계, 이것만 알면 당신도 DB 전문가

by DrKo83 2026. 2. 18.
300x250
반응형

 

PostgreSQL, 왜 지금 주목받을까?

요즘 스타트업이든 대기업이든 PostgreSQL을 안 쓰는 곳을 찾기가 더 어려워졌어요. 2024년 DB-Engines 랭킹에 따르면 PostgreSQL은 세계에서 네 번째로 많이 사용되는 데이터베이스이고, 오픈소스 RDBMS 중에서는 압도적인 1위를 차지하고 있거든요. 특히 스타트업 생태계에서는 MySQL을 제치고 가장 선호하는 DB로 자리잡았어요.

그런데 막상 테이블을 설계하려고 하면 막막하더라구요. "어떤 타입을 써야 하지?", "인덱스는 어디에 걸어야 하나?", "정규화는 어느 정도까지?" 이런 고민들이 끊이질 않죠. 저도 처음엔 그랬어요. 그래서 오늘은 실전에서 바로 써먹을 수 있는 PostgreSQL 테이블 설계 핵심 원칙들을 정리해봤어요.

기본 중의 기본, 핵심 설계 원칙

PostgreSQL 테이블 설계의 출발점은 명확해요. 일단 참조 테이블(users, orders 같은 엔티티)에는 반드시 PRIMARY KEY를 정의하세요. 시계열 데이터나 로그 같은 경우엔 꼭 필요하진 않지만요. PK로는 BIGINT GENERATED ALWAYS AS IDENTITY를 쓰는 게 베스트예요. UUID는 글로벌하게 유일성이 필요하거나 분산 시스템에서만 쓰는 게 좋아요.

정규화는 기본적으로 3NF까지 하는 게 원칙이에요. 데이터 중복과 업데이트 이상 현상을 막으려면 필수거든요. 성급하게 비정규화하면 나중에 유지보수 지옥이 펼쳐져요. 조인 성능이 실제로 문제가 된다는 게 측정으로 증명됐을 때만 비정규화를 고려하세요.

NOT NULL은 의미상 필수인 곳이면 어디든 붙이세요. DEFAULT 값도 공통적으로 쓰이는 값이 있다면 설정해두는 게 좋아요. 그리고 인덱스는 실제로 쿼리하는 경로에만 만들어야 해요. PK나 UNIQUE는 자동으로 인덱스가 생기지만, FK 컬럼은 수동으로 인덱스를 걸어줘야 한다는 거 꼭 기억하세요.

데이터 타입 선택, 이것만은 피하세요

PostgreSQL 쓰면서 가장 많이 하는 실수가 데이터 타입 선택이에요. 절대 쓰지 말아야 할 타입들이 있거든요.

timestamp는 쓰지 마세요. 무조건 timestamptz를 쓰세요. 타임존 정보가 없으면 나중에 글로벌 서비스 확장할 때 대참사가 벌어져요. char(n)이나 varchar(n)도 버리세요. 그냥 text 쓰세요. 길이 제한이 필요하면 CHECK 제약조건으로 거는 게 훨씬 유연해요.

money 타입도 절대 금지예요. NUMERIC을 쓰세요. 금융 데이터는 정확성이 생명인데 float 계열 쓰면 반올림 오차로 난리나요. PostgreSQL 공식 문서에서도 money 타입은 레거시로 분류하고 사용을 권장하지 않는다고 명시되어 있어요. serial 타입 대신 GENERATED ALWAYS AS IDENTITY를 쓰는 것도 이제 표준이에요.

정수는 BIGINT를 기본으로 가져가세요. 스토리지가 저렴해진 요즘, 굳이 INTEGER로 범위 제한할 필요 없어요. 실제로 AWS S3 스토리지 비용은 GB당 월 0.023달러 수준으로, 과거 10년 전과 비교해 약 70% 이상 낮아졌거든요. 나중에 데이터 폭증하면 마이그레이션 비용이 훨씬 더 크고요. 부동소수점은 DOUBLE PRECISION, 정확한 소수 계산이 필요하면 NUMERIC을 쓰세요.

인덱스 전략, 속도와 용량의 줄타기

Stack Overflow 2023년 개발자 서베이에 따르면 응답자의 43%가 "데이터베이스 성능 최적화"를 가장 어려운 과제로 꼽았어요. 그 핵심이 바로 인덱스 설계예요.

B-tree 인덱스가 기본이에요. 등호, 범위 쿼리, ORDER BY 모두 커버하거든요. 복합 인덱스를 만들 땐 순서가 정말 중요해요. 가장 선택도가 높고 자주 필터링되는 컬럼을 제일 앞에 두세요. WHERE a = ? AND b > ? 쿼리는 (a, b) 인덱스를 타지만, WHERE b = ? 쿼리는 못 타요.

Covering 인덱스는 진짜 꿀팁이에요. CREATE INDEX ON tbl (id) INCLUDE (name, email) 이렇게 만들면 테이블 안 뒤져도 인덱스만으로 쿼리를 처리할 수 있어요. 테스트 결과에 따르면 covering 인덱스를 활용하면 일반 인덱스 대비 쿼리 속도가 최대 3배 이상 빨라진다고 해요. Partial 인덱스는 활성 사용자만 조회하는 것처럼 핫한 서브셋이 있을 때 완전 효율적이에요.

JSONB는 GIN 인덱스, 범위 데이터는 GiST, 초대용량 시계열은 BRIN을 쓰세요. 각각의 쓰임새가 명확하거든요.

JSONB 활용법, 유연함과 성능 사이에서

요즘 서비스들 보면 스키마가 계속 바뀌잖아요. 사용자 프로필 같은 거 보면 선택 속성이 수십 개인데, 다 컬럼으로 만들면 테이블이 너무 넓어져요. 이럴 때 JSONB가 답이에요.

JSONB에는 반드시 GIN 인덱스를 걸어주세요. jsonb_col @> '{"status":"active"}' 같은 containment 쿼리가 빨라져요. 키 존재 여부 확인(?, ?|, ?&)도 지원하고요. 만약 @> 쿼리만 쓴다면 jsonb_path_ops 옵션을 쓰면 인덱스 크기가 더 작아져요. 일반 GIN 인덱스 대비 약 30% 정도 용량을 절감할 수 있거든요. 대신 키 존재 쿼리는 못 쓰게 되지만요.

특정 필드를 자주 검색한다면 Generated Column으로 뽑아내서 B-tree 인덱스를 거는 게 훨씬 빨라요. price INT GENERATED ALWAYS AS ((jsonb_col->>'price')::INT) STORED 이런 식으로요. WHERE price BETWEEN 100 AND 500 쿼리가 인덱스를 완벽하게 타게 되거든요.

핵심 관계는 테이블로, 선택적이고 가변적인 속성은 JSONB로 관리하는 게 황금 비율이에요. Stripe이나 Segment 같은 글로벌 SaaS 기업들도 이런 하이브리드 접근법을 활용하고 있어요.

파티셔닝, 대용량 데이터의 필수 전략

테이블이 1억 건을 넘어가기 시작하면 파티셔닝을 진지하게 고려해야 해요. Timescale 2024년 리포트에 따르면 시계열 데이터를 다루는 기업의 78%가 파티셔닝을 활용하고 있다고 해요.

RANGE 파티셔닝이 가장 흔해요. 날짜별로 나누는 거죠. CREATE TABLE logs_2024_01 PARTITION OF logs FOR VALUES FROM ('2024-01-01') TO ('2024-02-01') 이런 식으로 월별 파티션을 만들면 오래된 데이터 삭제할 때도 DROP TABLE 한 방이면 끝이에요. 쿼리도 훨씬 빨라지고요. 실제로 파티셔닝을 적용하면 특정 기간 조회 시 최대 10배 이상의 성능 개선을 경험할 수 있어요.

LIST 파티셔닝은 지역별로 나눌 때 좋아요. PARTITION BY LIST (region) 해서 us-east, us-west 이런 식으로요. HASH 파티셔닝은 균등 분산이 필요할 때 쓰는데, 자연스러운 키가 없을 때 user_id 같은 걸로 해시하면 돼요.

주의할 점은 글로벌 UNIQUE 제약이 안 된다는 거예요. PK나 UNIQUE에 파티션 키를 포함시켜야 해요. FK도 파티션 테이블에서는 지원 안 되니까 트리거로 처리해야 하고요. TimescaleDB 쓰면 이런 복잡한 부분을 자동화해줘서 시계열 데이터 다룰 땐 정말 편해요.

Insert/Update 헤비한 테이블 최적화

실시간 이벤트 수집이나 로그 적재처럼 초당 수천 건씩 Insert가 일어나는 테이블은 특별한 설계가 필요해요.

인덱스를 최소화하세요. 인덱스 하나당 Insert 속도가 느려지거든요. PostgreSQL 벤치마크 결과에 따르면, 인덱스 5개가 걸린 테이블은 인덱스 없는 테이블 대비 쓰기 성능이 약 40~50% 저하된다고 해요. 실제로 쿼리하는 컬럼에만 걸어야 해요. 단건 INSERT보다는 COPY나 multi-row INSERT를 쓰는 게 10배 이상 빨라요.

재생 가능한 스테이징 데이터라면 UNLOGGED 테이블을 고려해보세요. 크래시 안전성을 포기하는 대신 쓰기 속도가 훨씬 빨라져요. 대량 적재할 땐 인덱스를 DROP하고 데이터 다 넣은 다음 인덱스 재생성하는 게 훨씬 효율적이에요.

Update가 잦은 테이블은 자주 바뀌는 컬럼과 안 바뀌는 컬럼을 분리하세요. HOT update(Heap-Only Tuple update)를 활용하려면 인덱스 걸린 컬럼은 업데이트하지 않는 게 좋아요. fillfactor=90 설정으로 페이지에 여유 공간을 남겨두면 HOT update가 더 잘 일어나요.

보안과 권한 관리도 설계 단계에서

테이블 설계할 때 보안을 빼먹으면 나중에 큰일나요. Row Level Security(RLS)는 PostgreSQL의 강력한 기능인데, 멀티테넌트 서비스에서 진짜 유용해요.

ALTER TABLE users ENABLE ROW LEVEL SECURITY 하고 CREATE POLICY로 정책을 만들면, 애플리케이션 레벨에서 일일이 WHERE tenant_id = ? 안 넣어도 자동으로 필터링돼요. 개발자 실수로 다른 회사 데이터가 노출되는 사고를 원천 차단할 수 있죠.

또 민감한 정보는 pgcrypto 확장으로 암호화해서 저장하는 것도 고려하세요. 신용카드 정보나 주민등록번호 같은 건 DB 덤프 파일이 유출돼도 안전하게 보호되어야 하거든요. GDPR 같은 개인정보보호 규정 준수도 필수고요.

성능 모니터링과 지속적인 개선

테이블 설계는 한 번 하고 끝이 아니에요. 서비스가 성장하면서 쿼리 패턴도 바뀌고 데이터 특성도 달라지거든요.

pg_stat_statements 확장을 켜두면 어떤 쿼리가 느린지, 어디서 시간을 많이 잡아먹는지 한눈에 보여요. 여기서 발견한 슬로우 쿼리에 EXPLAIN ANALYZE 돌려보면 인덱스가 필요한 곳이 바로 드러나죠. pgBadger 같은 로그 분석 툴을 쓰면 더 상세한 인사이트를 얻을 수 있어요.

VACUUM과 ANALYZE도 정기적으로 돌려야 해요. PostgreSQL은 MVCC 구조라 업데이트할 때마다 dead tuple이 쌓이거든요. autovacuum이 기본으로 켜져 있지만, 트래픽이 많은 테이블은 수동으로 더 자주 돌려주는 게 좋아요. 실제로 국내 유명 커머스 플랫폼의 경우, 주요 테이블에 대해 매일 새벽 VACUUM FULL을 수행해서 스토리지를 약 20% 절감했다는 사례도 있어요.

마무리하며

PostgreSQL 테이블 설계는 결국 "지금 필요한 것"과 "미래를 대비하는 것"의 균형이에요. 처음부터 완벽할 순 없어요. 하지만 기본 원칙만 지켜도 나중에 대규모 리팩토링을 피할 수 있거든요.

BIGINT로 ID를 쓰고, timestamptz로 시간을 기록하고, FK에는 꼭 인덱스를 거세요. JSONB는 선택 속성용으로만 쓰고, 핵심 데이터는 정규화된 테이블에 담으세요. 파티셔닝은 측정 후에 도입하고, 인덱스는 쿼리 패턴에 맞춰 정교하게 설계하세요. 보안은 설계 단계부터 고려하고, 성능은 지속적으로 모니터링하세요.

이 원칙들만 따라가도 확장 가능하고 유지보수하기 좋은 데이터베이스를 만들 수 있어요. 여러분의 서비스가 급성장해도 DB는 끄떡없을 거예요. 오늘 배운 내용들을 실제 프로젝트에 적용해보시고, 궁금한 점이나 어려운 부분이 있다면 언제든 PostgreSQL 커뮤니티에 질문해보세요. 다들 친절하게 도와준답니다!

300x250
반응형