Skip to main content
Summary

PostgreSQL은 어디에 쓰이는 데이터베이스인가

PostgreSQL은 오픈 소스 객체-관계형 데이터베이스다. 관계형 데이터베이스의 기본인 SQL을 중심에 두면서도, JSON, 배열, 사용자 정의 타입, 확장 기능처럼 현대 애플리케이션에서 필요한 기능을 폭넓게 제공한다. PostgreSQL 공식 소개에서도 PostgreSQL을 "SQL 언어를 사용하고 확장하는 오픈 소스 객체-관계형 데이터베이스 시스템"으로 설명한다1.

처음 데이터베이스를 배우면 MySQL, SQLite, Oracle, SQL Server 같은 이름도 함께 보게 된다. 이들은 모두 데이터를 저장하고 조회한다는 점에서는 비슷하지만, 자주 선택되는 상황이 조금씩 다르다.

SQLite는 별도 서버 없이 파일 하나로 동작한다. 모바일 앱, 데스크톱 프로그램, 작은 도구, 테스트 환경에 잘 맞는다. 설치와 배포가 단순하지만, 여러 사용자가 동시에 많은 쓰기 작업을 하는 서버용 시스템에는 보통 다른 선택지를 고려한다.

MySQL은 웹 서비스에서 오래 쓰여 왔다. 게시판, 쇼핑몰, 콘텐츠 관리 시스템처럼 읽기와 쓰기가 비교적 단순한 형태로 반복되는 서비스에서 많이 사용된다. 생태계가 크고 자료가 많아 입문 장벽이 낮다.

상용 DB인 Oracle, SQL Server는 대기업 시스템, 금융, 제조, 공공기관처럼 벤더 지원, 관리 도구, 기존 인프라와의 통합이 중요한 곳에서 많이 사용된다. 라이선스 비용이 있지만, 조직이 요구하는 지원 체계와 운영 표준을 함께 제공한다.

PostgreSQL은 그 사이에서 넓은 영역을 차지한다. 오픈 소스이면서도 복잡한 SQL, 트랜잭션, 동시성 처리, 확장 기능이 강하다. 예를 들어 다음과 같은 상황에서 자주 선택된다.

  • 쇼핑몰에서 주문, 결제, 재고, 정산 데이터를 정확하게 다뤄야 할 때

  • SaaS 서비스에서 고객별 데이터와 권한을 엄격하게 분리해야 할 때

  • 로그나 이벤트 데이터를 모아 분석 쿼리를 실행해야 할 때

  • 위치 정보를 다루기 위해 PostGIS 같은 확장을 사용해야 할 때

  • 기본 업무 데이터는 테이블로 관리하되, 일부 유연한 속성은 JSON으로 저장해야 할 때

PostgreSQL을 배운다는 것은 단순히 SELECT 문법을 외우는 일이 아니다. 데이터를 어떤 모양으로 저장할지, 여러 사용자가 동시에 접근할 때 어떻게 안전하게 처리할지, 느린 쿼리를 어떻게 찾고 고칠지 배우는 과정이다.

PostgreSQL을 배우기 전에 알아야 할 것들AI 생성 이미지

관계형 데이터베이스의 큰 그림

관계형 데이터베이스는 데이터를 표처럼 저장한다. 이 표를 테이블이라고 부른다. 테이블의 가로 한 줄은 행, 세로 한 칸의 항목은 열이다.

쇼핑몰 주문 시스템을 예로 들어 보자. 고객, 상품, 주문을 하나의 거대한 표에 모두 넣을 수도 있다.

고객명 | 고객이메일 | 상품명 | 상품가격 | 주문수량 | 주문일

처음에는 단순해 보인다. 하지만 같은 고객이 여러 번 주문하면 고객 이메일이 계속 반복된다. 상품 가격이 바뀌었을 때 과거 주문 금액까지 흔들릴 수 있다. 고객 정보만 수정하고 싶은데 주문 데이터까지 함께 건드려야 할 수도 있다.

그래서 관계형 데이터베이스에서는 데이터를 주제별 테이블로 나눈다.

CREATE TABLE customers (
    customer_id bigint PRIMARY KEY,
    name text NOT NULL,
    email text NOT NULL UNIQUE
);

CREATE TABLE products (
    product_id bigint PRIMARY KEY,
    name text NOT NULL,
    price numeric(12, 2) NOT NULL
);

CREATE TABLE orders (
    order_id bigint PRIMARY KEY,
    customer_id bigint NOT NULL REFERENCES customers(customer_id),
    ordered_at timestamptz NOT NULL,
    status text NOT NULL
);

CREATE TABLE order_items (
    order_id bigint NOT NULL REFERENCES orders(order_id),
    product_id bigint NOT NULL REFERENCES products(product_id),
    quantity integer NOT NULL,
    unit_price numeric(12, 2) NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

여기에는 앞으로 계속 만날 중요한 개념이 들어 있다.

customers, products, orders, order_items는 테이블이다. 각 테이블은 한 가지 주제의 데이터를 맡는다. customer_id, product_id, order_id는 기본 키다. 기본 키는 한 행을 다른 행과 구분하는 값이다. 고객 번호가 101번이면, 그 고객을 가리킬 때 이름보다 customer_id = 101이 더 안전하다. 이름은 같을 수 있고 바뀔 수도 있기 때문이다.

orders.customer_id는 외래 키다. 외래 키는 다른 테이블의 기본 키를 참조한다. 주문은 반드시 어떤 고객의 주문이어야 하므로, orders.customer_id가 customers.customer_id를 가리킨다. 존재하지 않는 고객 번호로 주문을 만들려고 하면 데이터베이스가 막아 준다.

order_items는 주문과 상품을 연결한다. 한 주문에는 여러 상품이 들어갈 수 있고, 한 상품도 여러 주문에 포함될 수 있다. 이런 관계를 풀기 위해 중간 테이블을 둔다. 여기서 unit_price는 주문 당시의 상품 가격을 남기는 열이다. 상품의 현재 가격인 products.price가 나중에 바뀌더라도, 이미 발생한 주문의 금액은 order_items.unit_price를 기준으로 계산할 수 있다. 이 구조를 이해하면 조인이 왜 필요한지도 자연스럽게 보인다.

SELECT
    o.order_id,
    c.name AS customer_name,
    p.name AS product_name,
    oi.quantity,
    oi.unit_price
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.order_id = 1001;

이 쿼리는 흩어진 테이블을 연결해 "1001번 주문에 어떤 고객이 어떤 상품을 몇 개 샀는가"를 보여 준다. 관계형 데이터베이스에서 조인은 분리된 데이터를 다시 읽기 좋은 모양으로 합치는 방법이다.

인덱스와 트랜잭션은 왜 필요한가

테이블에 데이터가 몇십 건뿐이면 어떤 쿼리도 빠르게 보인다. 하지만 주문이 천만 건이 되면 이야기가 달라진다. 특정 고객의 주문을 찾기 위해 매번 전체 주문 테이블을 처음부터 끝까지 읽는다면 응답 시간이 길어진다.

이때 인덱스를 사용한다. 인덱스는 책의 찾아보기와 비슷하다. 책에서 "트랜잭션"이라는 단어가 나온 페이지를 찾을 때 모든 페이지를 읽지 않고 찾아보기를 먼저 보듯, 데이터베이스도 인덱스를 사용해 필요한 행으로 빨리 이동할 수 있다.

CREATE INDEX idx_orders_customer_id
ON orders (customer_id);

이 인덱스는 고객 번호로 주문을 자주 찾을 때 도움이 된다. 그러나 인덱스는 공짜가 아니다. 데이터를 추가, 수정, 삭제할 때 인덱스도 함께 갱신해야 한다. 그래서 "많이 만들수록 좋다"가 아니라 "자주 검색하고 조인하고 정렬하는 기준에 맞게 만든다"가 원칙이다.

트랜잭션은 여러 작업을 하나의 작업처럼 묶는 방법이다. 주문을 생성할 때는 보통 주문 행을 만들고, 주문 상품을 넣고, 재고를 줄이고, 결제 기록을 남긴다. 이 중 하나만 성공하고 나머지가 실패하면 데이터가 어긋난다.

아래 예시는 앞에서 만든 orders와 order_items 테이블만 사용해 트랜잭션의 모양을 보여 준다.

BEGIN;

INSERT INTO orders (order_id, customer_id, ordered_at, status)
VALUES (1001, 101, now(), 'created');

INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (1001, 501, 2, 15000.00);

UPDATE orders
SET status = 'paid'
WHERE order_id = 1001;

COMMIT;

실무에서는 이 사이에 재고 차감, 결제 기록 생성, 쿠폰 사용 처리 같은 작업이 더 들어갈 수 있다. 실제 재고 예제는 뒤에서 더 정확하게 다룬다. 지금은 BEGIN과 COMMIT 사이의 작업이 하나의 단위라는 점만 기억하면 된다. 중간에 문제가 생기면 ROLLBACK으로 되돌릴 수 있다. 주문 시스템에서 트랜잭션은 선택 기능이 아니라 데이터의 신뢰성을 지키는 기본 장치다.

PostgreSQL의 특징을 실무 언어로 이해하기

PostgreSQL의 기능 목록은 길다. 하지만 처음부터 모두 외울 필요는 없다. 먼저 실무에서 자주 의미가 생기는 네 가지를 잡으면 된다.

첫째, PostgreSQL은 표준 SQL을 중요하게 다룬다. 데이터베이스마다 SQL 문법이 조금씩 다르지만, 표준에 가까운 문법을 익히면 다른 데이터베이스를 이해하기도 쉬워진다. PostgreSQL은 SQL 표준을 폭넓게 지원하며, PostgreSQL 18 기준 SQL:2023 Core의 필수 기능 중 상당수를 충족한다고 공식 문서가 설명한다2. 이 책에서도 특정 제품에만 갇힌 요령보다 오래 쓸 수 있는 SQL 감각을 먼저 다룬다.

둘째, PostgreSQL은 확장성이 좋다. 확장성은 단순히 "많은 데이터를 처리한다"는 뜻만이 아니다. 필요한 기능을 데이터베이스 안에 추가할 수 있다는 뜻도 있다. 대표적으로 PostGIS는 위치 정보와 공간 연산을 PostgreSQL에서 다룰 수 있게 해 준다. pg_stat_statements는 어떤 쿼리가 시간을 많이 쓰는지 확인할 때 유용하다. 확장은 운영 환경에서 신중히 선택해야 하지만, 잘 사용하면 별도 시스템을 늘리지 않고 문제를 해결할 수 있다.

셋째, PostgreSQL은 JSON을 다룰 수 있다. 관계형 데이터베이스라고 해서 모든 데이터가 딱딱한 열 구조에만 들어가야 하는 것은 아니다. 상품마다 옵션이 다르거나, 외부 API 응답 일부를 함께 저장해야 할 때 JSON이 편리하다.

CREATE TABLE product_events (
    event_id bigint PRIMARY KEY,
    product_id bigint NOT NULL,
    payload jsonb NOT NULL,
    created_at timestamptz NOT NULL
);

다만 JSON은 만능이 아니다. 고객 번호, 주문 번호, 결제 금액처럼 검색과 검증이 중요한 값은 보통 별도 열로 두는 편이 낫다. JSON은 구조가 자주 바뀌거나 부가 정보 성격이 강한 데이터에 맞다.

넷째, PostgreSQL은 동시성 처리를 위해 MVCC를 사용한다. MVCC는 다중 버전 동시성 제어라는 뜻이다. 쉽게 말해, 한 사용자가 데이터를 읽는 동안 다른 사용자의 작업을 불필요하게 오래 막지 않도록 여러 버전의 데이터를 관리하는 방식이다. 덕분에 많은 사용자가 동시에 조회하고 수정하는 서비스에서 안정적인 처리 모델을 제공한다. 물론 MVCC가 모든 충돌을 자동으로 없애 주지는 않는다. 같은 재고를 동시에 줄이는 상황처럼 정확한 제어가 필요한 문제는 트랜잭션 격리 수준, 잠금, 제약 조건을 함께 이해해야 한다.

이 책에서 사용할 학습 방식

이 책은 PostgreSQL 16~18 계열에서 공통으로 중요한 원리와 실무 패턴을 중심으로 설명한다. 버전에 따라 새로 생긴 기능이나 동작 차이가 있는 부분은 해당 장에서 따로 주의점을 적는다. 처음부터 모든 기능을 넓게 훑기보다, 실제로 데이터를 설계하고 쿼리를 작성하고 운영 문제를 확인하는 흐름으로 진행한다.

샘플 데이터는 주로 네 가지 업무 상황을 사용한다.

  • 쇼핑몰: 고객, 상품, 주문, 결제, 재고

  • 게시판: 사용자, 글, 댓글, 좋아요

  • 로그 분석: 접속 이벤트, 클릭 이벤트, 에러 로그

  • 정산 데이터: 판매 금액, 수수료, 환불, 월별 집계

실습 쿼리는 가능하면 바로 실행할 수 있는 형태로 제시한다. 쿼리를 읽을 때는 다음 순서로 보면 좋다.

  1. 어떤 테이블에서 읽는가: FROM

  2. 어떤 테이블과 연결하는가: JOIN

  3. 어떤 조건으로 줄이는가: WHERE

  4. 어떤 단위로 묶는가: GROUP BY

  5. 어떤 열을 결과로 보여 주는가: SELECT

  6. 어떤 순서로 보여 주는가: ORDER BY

SQL은 영어 문장처럼 위에서 아래로 읽히지만, 데이터베이스가 처리하는 논리 순서는 조금 다르다. 그래서 처음에는 SELECT보다 FROM과 JOIN을 먼저 보는 습관이 도움이 된다.

실행 결과는 "값이 나왔다"에서 멈추지 말고 세 가지를 확인한다.

  • 행 수가 예상과 맞는가

  • 금액, 개수, 날짜 범위가 업무 규칙과 맞는가

  • 중복되거나 빠진 데이터가 없는가

예를 들어 고객별 주문 수를 구했는데 한 고객의 주문 수가 실제보다 두 배로 보이면 조인 조건이 잘못되었을 가능성이 있다. 합계 금액이 예상보다 크면 주문 상품과 결제 내역을 동시에 조인하면서 행이 늘어났을 수 있다. SQL 학습에서 중요한 것은 문법을 맞히는 것뿐 아니라 결과가 맞는지 의심하는 습관이다.

먼저 가져갈 기준

PostgreSQL을 처음 배울 때 모든 용어가 한꺼번에 나오면 복잡하게 느껴질 수 있다. 이 장에서는 다음 기준만 확실히 가져가면 된다.

테이블은 데이터를 주제별로 나누는 단위다. 기본 키는 행을 식별한다. 외래 키는 테이블 사이의 관계를 만든다. 인덱스는 조회를 빠르게 하지만 쓰기 비용을 늘린다. 트랜잭션은 여러 변경 작업을 안전한 하나의 단위로 묶는다.

PostgreSQL은 이 기본기를 바탕으로 복잡한 SQL, JSON, 확장 기능, 동시성 처리, 운영 기능을 제공한다. 앞으로의 장에서는 이 요소들을 하나씩 분리해서 배우고, 다시 하나의 시스템으로 연결한다. 문법을 외우기 전에 "왜 이 구조가 필요한가"를 먼저 생각하면 PostgreSQL은 훨씬 명확하게 보인다.

참고

1PostgreSQL 공식 소개

2PostgreSQL 공식 문서: SQL 표준 지원 및 기능 범위

책 목록

PostgreSQL 입문부터 실전 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기