pg_durable: PostgreSQL 안에서 돌아가는 durable execution, 실무에서 쓸만한가
Microsoft가 pg_durable이라는 PostgreSQL 확장을 오픈소스로 풀었다. Hacker News에서 410점, 한국 커뮤니티(GeekNews)에도 바로 올라온 걸 보면 관심도가 꽤 높다. 한 줄로 요약하면 "Temporal이나 AWS Step Functions 같은 durable execution을 외부 인프라 없이 Postgres 안에서 처리한다"는 물건이다.
나처럼 인프라를 굴려본 사람 입장에서 이게 왜 흥미로운지, 그리고 어디서 발목을 잡힐지 실무 관점에서 정리해본다.
왜 지금 이게 화제인가
durable execution이라는 개념 자체는 새롭지 않다. 결제 처리, 회원가입 후 이메일 발송, 외부 API 여러 개를 순서대로 호출하는 작업 같은 걸 생각해보자. 중간에 워커가 죽거나 네트워크가 끊기면 어디까지 처리됐는지 알 수가 없다. 그래서 우리는 보통 이런 걸 만든다.
- 작업 상태를 DB 테이블에 기록 (pending → processing → done)
- Redis나 RabbitMQ로 큐를 깔고 워커를 띄움
- 실패하면 재시도하는 로직, 중복 실행 막는 idempotency 키
- 스케줄링 필요하면 cron이나 별도 스케줄러
이걸 제대로 하려면 Temporal, AWS Step Functions, Inngest 같은 솔루션을 도입하거나, 직접 상태머신을 짜야 한다. 문제는 이게 다 외부 인프라를 하나 더 추가한다는 거다. 운영 포인트가 늘고, 모니터링 대상이 늘고, 장애 지점이 늘어난다.
pg_durable의 핵심 주장은 이거다. "이미 Postgres 쓰고 있잖아? 컨테이너도 외부 서비스도 필요 없이 Postgres와 백그라운드 워커만으로 처리하자." 트랜잭션, 상태 저장, 재시도가 전부 DB 안에서 끝난다는 게 매력 포인트다.
동작 원리 — 체크포인트가 핵심이다
공식 저장소 설명에 따르면 작은 SQL DSL로 재시도, 스케줄링, 병렬 fan-out, 조건 분기를 표현한다. 그리고 가장 중요한 건 모든 단계가 PostgreSQL에 상태를 체크포인트로 저장한다는 점이다.
비유하자면 게임 세이브 포인트다. 보스전 직전에 세이브해두면, 죽어도 처음부터가 아니라 세이브 지점부터 다시 시작한다. durable execution도 똑같다. 단계마다 "여기까지 했다"를 DB에 기록하니까, 워커가 죽고 다시 살아나도 이미 끝낸 단계는 건너뛰고 다음부터 이어간다.
일반적인 durable function의 흐름은 대략 이런 모양이다. (정확한 문법은 저장소 문서 확인 필요)
-- durable function 정의 (개념적 예시)
-- step 1: 결제 호출
-- step 2: 재고 차감
-- step 3: 알림 발송
-- 각 step 결과가 체크포인트로 저장됨
SELECT durable.call('process_order', jsonb_build_object('order_id', 1234));
여기서 step 2를 처리하다가 워커가 죽었다고 하자. 재시작하면 엔진은 "step 1은 이미 완료(체크포인트 있음), step 2부터 다시"라고 판단한다. step 1의 결제 호출이 두 번 일어나지 않는다는 게 핵심 가치다.
외부 큐를 안 쓰는 대신 백그라운드 워커가 Postgres를 폴링하거나 알림(LISTEN/NOTIFY 추정)으로 깨어나서 다음 step을 진행하는 구조로 보인다. 이 부분 구현 방식은 직접 소스 확인이 필요하다.
실무 관점 — 좋은데, 만능은 아니다
매력적인 그림이지만 실무에 넣기 전에 따져봐야 할 게 있다.
1. DB가 워크로드 처리기까지 겸하게 된다
이게 가장 큰 트레이드오프다. 평소에 Postgres는 데이터 저장소다. 그런데 pg_durable을 쓰면 비즈니스 워크플로우 실행, 재시도, 스케줄링까지 DB가 떠안는다. 백그라운드 워커가 폴링을 돌리면 그만큼 DB 부하가 생긴다.
트래픽 작은 서비스에선 문제없다. 그런데 워크플로우가 초당 수천 건씩 돌아가는 규모라면, DB가 병목이 되는 순간 스토리지와 워크플로우 엔진이 동시에 죽는다. 관심사를 분리해놨으면 하나만 죽었을 일이다. 이건 "DB에 뭐든 넣자" 류 확장(pg_cron, pgmq 등)이 공통으로 갖는 숙명이다.
2. 운영 단순함 vs 확장성의 교환
스타트업 초기나 내부 도구, 트래픽 예측 가능한 백오피스 작업에는 정말 좋다. Temporal 클러스터 운영해본 사람은 알겠지만, 그거 띄우고 유지하는 것 자체가 일이다. 반면 pg_durable은 "extension 깔고 워커 띄우면 끝"에 가깝다. 인프라 한 덩어리가 통째로 사라진다.
하지만 트래픽이 커지고 워크플로우가 복잡해지면, 결국 전용 durable execution 플랫폼으로 갈아타야 하는 순간이 온다. 그때 마이그레이션 비용이 든다. 처음부터 "이 서비스는 절대 커질 일 없다" 또는 "커지면 그때 갈아탄다"는 판단이 서야 도입할 만하다.
3. 흔한 함정 — idempotency를 공짜로 주는 게 아니다
durable execution이 step 재실행을 건너뛰어준다고 해도, 외부 부수효과(side effect)의 멱등성은 여전히 네 책임이다. 예를 들어 step에서 외부 결제 API를 호출했는데 응답 받기 직전에 워커가 죽었다고 하자. 엔진 입장에선 "이 step 완료 기록이 없다 → 재시도"인데, 실제로는 결제가 이미 됐을 수 있다.
이런 케이스는 어떤 durable execution 솔루션을 써도 똑같이 발생한다. 외부 호출 쪽에 idempotency key를 같이 보내거나, 결제 상태를 먼저 조회하는 식으로 직접 방어해야 한다. "체크포인트 있으니 중복 안 일어나겠지"라고 믿으면 사고 난다.
4. 대안과 비교
- Temporal / Cadence: 대규모·복잡한 워크플로우의 정석. 무겁고 운영 부담 큼.
- AWS Step Functions: 매니지드라 운영 편하지만 AWS 락인, 비용, 디버깅 불편.
- pgmq + 직접 구현: Postgres 기반 큐만 쓰고 워크플로우 로직은 직접. 더 가볍지만 손이 많이 감.
- Inngest, Trigger.dev: 코드 중심 durable execution, DX 좋음. 외부 SaaS 의존.
- pg_durable: 이미 Postgres 쓰고, 추가 인프라 없이 적당한 규모를 처리하고 싶을 때.
참고로 아직 초기 단계 프로젝트로 보이니, 프로덕션에 바로 넣기보단 사이드 워크로드에서 먼저 검증하길 권한다. 버전 안정성, 마이그레이션 호환성은 직접 확인이 필요하다.
정리
한 줄 요약: pg_durable은 외부 워크플로우 인프라 없이 Postgres만으로 durable execution을 처리하는 확장이고, 운영 단순함을 얻는 대신 DB에 부하를 몰아주는 트레이드오프가 있다.
- 이럴 때 써라: 이미 Postgres 중심 스택이고, 트래픽 규모가 크지 않으며, Temporal까지 도입하긴 과한 백오피스/내부 작업/중소 서비스의 비동기 워크플로우.
- 피해라: 초당 수천 건 워크플로우, DB가 이미 병목인 환경, 워크플로우 엔진 장애와 DB 장애를 분리해야 하는 미션 크리티컬 시스템.
개인적으론 방향성이 좋다고 본다. "또 인프라 하나 추가"에 지친 팀에겐 충분히 매력적이다. 다만 멱등성은 여전히 네가 챙겨야 하고, 규모가 커질 때의 출구 전략은 미리 그려두고 들어가는 게 안전하다.