같은 히스토리 테이블 코드를 반복 작성하느라 지쳐 Postgres 확장을 만들었다.

발행: (2026년 6월 6일 AM 02:31 GMT+9)
7 분 소요
원문: Dev.to

출처: Dev.to

제가 계속 마주치던 문제가 있습니다. 처음에는 인턴십 동안, 그 다음에는 이후에 맡은 프리랜스 프로젝트에서 말이죠. 두 프로젝트 모두 PostgreSQL을 사용했고, 어느 시점에서든 같은 질문에 대한 답이 필요했습니다: “이 데이터는 이전에 어떻게 생겼을까?”
감사 로그가 될 수도 있고, 고객이 “15일에 가격이 얼마였나요?”라고 물어볼 수도 있습니다. 혹은 잘못된 배포로 인해 많은 행이 삭제돼서 누군가가 두 시간 전 상태를 복구해야 할 수도 있죠. 매번 답은 같은 번거로운 절차였습니다: created_at·updated_at 컬럼을 추가하고, 원본을 그대로 복제한 별도 히스토리 테이블을 만들고, INSERT, UPDATE, DELETE마다 트리거를 작성해 데이터를 채우고, 구간 경계가 정확한지 애써 확인하는 것이었습니다. 프리랜스 프로젝트에서는 처음 시도에서 실수를 해서 특정 타임스탬프에서는 행이 두 번 카운트되거나 전혀 나타나지 않았습니다. 결국 전체를 다시 고쳐야 했죠. 그래서 저는 이 문제를 제대로 해결할 수 있는 도구를 직접 만들었습니다.

pgtime가 하는 일

SELECT pgtime.attach('orders');

그게 전부입니다. 이제부터 orders 테이블에 대한 모든 INSERT, UPDATE, DELETE는 자동으로 ordershistory라는 그림자 히스토리 테이블에 기록됩니다. 기존 스키마를 건드릴 필요도 없고, 기존 쿼리도 바뀌지 않으며, 추적은 백그라운드에서 조용히 동작합니다. attach()를 호출하면 확장이 해당 테이블의 정확한 스키마에 맞는 헬퍼 함수를 동적으로 컴파일합니다. 따라서 과거 데이터를 조회할 때도 번거로운 타입 캐스팅 없이 깔끔한 네이티브 쿼리를 사용할 수 있습니다.

-- 2026년 1월 15일 10시(UTC) 기준 전체 테이블 스냅샷 가져오기
SELECT * FROM ordersasof('2026-01-15 10:00:00+00');

특정 행의 전체 변경 이력을 보고 싶다면:

-- 주문 ID 42의 전체 감사 로그 조회
SELECT price, sysfrom, pgtimeop FROM ordershistory(42);

C 트리거를 사용하고 PL/pgSQL을 쓰지 않은 이유

행 변화를 캡처하는 트리거는 C로 작성되었습니다. 솔직히 말하면 성능 때문입니다. PL/pgSQL은 각 행 연산마다 오버헤드가 발생하는데, 쓰기량이 많은 테이블에서는 그 비용이 빠르게 누적됩니다. C 트리거는 더 낮은 레벨에서 동작하므로 성능이 뛰어납니다. 이를 입증하기 위해 Postgres 16 컨테이너 안에서 10,000건의 연산에 대한 베이스라인 벤치마크를 수행했습니다. 범위 인덱싱(tstzrange·GiST 인덱스) 오버헤드가 있더라도 C 트리거는 다음과 같은 속도를 기록했습니다:

  • INSERT ≈ 63,700 ops/sec
  • UPDATE ≈ 20,100 ops/sec (구버전을 닫고 새 버전을 삽입하는 이중 쓰기)

SDK도 제공됩니다

순수 SQL만으로 사용하도록 제한하고 싶지 않아 Node/TypeScript와 Python용 얇은 래퍼를 만들었습니다.

TypeScript

import { Pool } from 'pg';
import { PgTime } from 'pgtime-js';

const pool = new Pool({ connectionString: process.env.DATABASEURL });
const pt = new PgTime(pool);
await pt.attach('orders');

interface Order {
  id: number;
  item: string;
  price: number;
}

const snapshot = await pt.asOf('orders', '2026-01-15T10:00:00Z');
const auditTrail = await pt.history('orders', 42);

Python

from pgtime import PgTime
import psycopg2

conn = psycopg2.connect("postgres://...")
pt = PgTime(conn)

pt.attach("orders")
snapshot = pt.asof("orders", "2026-01-15T10:00:00Z")
logs = pt.history("orders", 42)

터미널에서 바로 사용할 수 있는 Go CLI도 있습니다.

pgtime attach orders --db postgres://localhost/mydb
pgtime history orders --id 42 --db postgres://localhost/mydb
pgtime diff orders --from "2026-01-01" --to "2026-02-01" --db postgres://localhost/mydb

현재 상황

현재 v0.1.1‑alpha 버전이며, 트랜잭션 타임(데이터베이스 시스템 시계)은 완전히 동작합니다. 비즈니스 타임(사용자가 직접 정의하는 시간 구간)은 v0.2 로드맵에 포함돼 있지만 아직 구현되지 않았습니다. SDK는 아직 npm이나 PyPI에 공개되지 않아 로컬 클론을 통해 설치해야 합니다. 다음 릴리스에서 변경될 예정입니다.

설치는 개발·테스트용 Docker 기반이 기본이며, Postgres 개발 헤더가 있다면 C 확장을 직접 컴파일할 수도 있습니다—README에 두 방법 모두 자세히 안내돼 있습니다.

확인해보고 싶다면

GitHub: https://github.com/Sanidhyavijay24/pgtime

여러분의 의견을 듣고 싶습니다

이 문제를 직접 겪어보셨거나 다른 방식으로 해결한 경험이 있다면 언제든 이야기해 주세요. 구현에 어색하거나 잘못된 부분이 있다면 지적해 주시면 감사하겠습니다. 아직 초기 단계라 별보다 실제 피드백이 더 큰 도움이 됩니다.

0 조회
Back to Blog

관련 글

더 보기 »

모바일 한여름 열풍

!Cover image for Mobile Midsommer Madnesshttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploa...