Tech_News

Postgres 하나로 Durable Workflow 짜기 — DBOS 방식이 실무에서 먹히는 이유

TeEm0 2026. 5. 29. 13:52

Postgres 하나로 Durable Workflow 짜기 — DBOS 방식이 실무에서 먹히는 이유

오늘 HN 목록 보다가 "Building durable workflows on Postgres"(DBOS) 글이 292점 받고 올라와 있길래 골랐다. AI 관련 글이 절반인 와중에 이건 인프라/백엔드 하는 사람한테 훨씬 실용적인 주제다. 워크플로우 오케스트레이션이라고 하면 보통 Temporal, Airflow, Step Functions 떠올리는데, "그거 그냥 Postgres로 다 되는 거 아니냐"는 주장이라 한번 정리해볼 만하다.

왜 지금 이게 화제인가

현업에서 결제, 주문, 정산 같은 워크플로우 짜본 사람은 다 안다. "여러 단계로 이뤄진 작업이 중간에 죽으면 어디부터 다시 해야 하나" 이게 진짜 골칫거리다.

예를 들어 결제 플로우가 이렇다고 치자.

1. 재고 차감
2. 결제 승인 (외부 PG 호출)
3. 포인트 적립
4. 알림 발송

2번에서 PG 호출하다가 서버 프로세스가 죽었다. 결제는 됐는데 포인트는 안 붙었다. 재배포로 인스턴스가 내려갔을 수도 있고, OOM으로 죽었을 수도 있다. 이 "중간에 죽어도 정확히 멈춘 지점부터 다시 이어서 한다"는 게 durable execution(내구성 있는 실행)의 핵심이다.

지금까지 이걸 제대로 하려면 Temporal 같은 별도 워크플로우 엔진을 띄워야 했다. 근데 그게 운영 부담이 만만치 않다. DBOS의 주장은 "이미 너희가 쓰는 Postgres 트랜잭션이면 충분하다"는 거다.

핵심: 어떻게 Postgres만으로 되나

아이디어 자체는 의외로 단순하다. 각 단계(step)의 실행 결과를 Postgres 테이블에 기록해두고, 워크플로우가 재시작되면 이미 완료된 단계는 건너뛰고 결과만 읽어온다. 이게 흔히 말하는 메모이제이션(memoization)이다.

DBOS 라이브러리 기준으로 쓰면 대충 이런 모양이다.

import { DBOS } from "@dbos-inc/dbos-sdk";

class Checkout {
  @DBOS.step()
  static async chargePayment(orderId: string) {
    // 외부 PG 호출
    return await pg.charge(orderId);
  }

  @DBOS.step()
  static async addPoints(userId: string, amount: number) {
    return await pointService.add(userId, amount);
  }

  @DBOS.workflow()
  static async run(orderId: string, userId: string) {
    const result = await Checkout.chargePayment(orderId);
    await Checkout.addPoints(userId, result.amount);
    return result;
  }
}

여기서 마법은 @DBOS.step()에 있다. 이 데코레이터가 붙은 함수는 실행되면 결과가 Postgres에 저장된다. 만약 addPoints 직전에 프로세스가 죽었다가 워크플로우가 재시작되면, chargePayment는 다시 호출되지 않고 저장된 결과를 그대로 반환한다. 이미 결제가 된 걸 또 긁는 사고를 막아주는 구조다.

비유하자면 게임 세이브 포인트다. 보스 잡고 세이브 찍었으면, 다음 구간에서 죽어도 보스부터 다시 잡진 않는다. 워크플로우 상태와 각 스텝 결과가 전부 DB 트랜잭션 안에서 원자적으로 커밋되니까, "절반만 저장된" 어정쩡한 상태가 안 생긴다. 이게 Postgres의 ACID를 그대로 빌려쓰는 부분이다.

그리고 워크플로우 실행 코드 자체가 애플리케이션 프로세스 안에서 돌아간다는 점이 Temporal과 가장 다르다. Temporal은 워커-서버 구조로 별도 클러스터가 필요한데, DBOS는 라이브러리 임베드 방식이라 "내 앱 + Postgres" 조합이면 끝난다.

실무 관점: 도입 전에 따져볼 것들

1) 스텝은 멱등(idempotent)하게 짜야 한다. 이건 durable execution 도구를 뭘 쓰든 공통이다. DBOS가 완료된 스텝을 건너뛴다고 해도, "정확히 한 번 실행됐는지" 보장은 스텝이 커밋되기 직전에 죽는 경계 케이스에서 까다롭다. 외부 PG 호출 같은 건 idempotency key를 같이 넘겨서 PG 쪽에서도 중복 승인을 막아야 안전하다. 라이브러리만 믿고 멱등성 설계를 생략하면 언젠가 중복 결제로 사고 난다.

2) Postgres가 단일 장애 지점이자 병목이 된다. 모든 워크플로우 상태가 한 DB로 몰린다. 워크플로우 처리량이 높아지면 스텝마다 INSERT/UPDATE가 발생하니 DB write 부하가 커진다. 트래픽 큰 서비스라면 워크플로우 전용 DB를 분리하거나, 어차피 쓰는 Postgres라도 커넥션 풀과 vacuum 전략을 미리 생각해둬야 한다. "Postgres is all you need"는 규모가 적당할 때 맞는 말이지, 무한정 확장된다는 뜻은 아니다.

3) 스텝 시그니처를 함부로 못 바꾼다. 진행 중인 워크플로우가 DB에 남아있는데 코드에서 스텝 순서를 바꾸거나 추가하면, 재시작 시 저장된 상태와 코드가 안 맞아 깨질 수 있다. 이건 Temporal의 versioning 이슈와 같은 종류의 함정이다. 워크플로우 코드 변경은 배포 전략을 따로 신경 써야 한다(공식 문서에서 버전 관리 정책 확인 필요).

대안과 비교

  • Temporal: 기능과 생태계는 가장 성숙. 대신 별도 클러스터 운영 부담. 규모 크고 워크플로우가 복잡하면 여전히 강력한 선택.
  • Airflow: 배치/데이터 파이프라인용. 실시간 트랜잭션성 워크플로우엔 결이 안 맞는다.
  • Step Functions: AWS 락인. 매니지드라 편하지만 로컬 개발/디버깅이 불편하고 상태 전이마다 비용이 붙는다.
  • 직접 구현: outbox 패턴 + 상태 테이블로 비슷하게 만들 수 있다. DBOS는 이 패턴을 라이브러리로 추상화해준 거라고 보면 된다.

개인적으로는 이미 Postgres 박혀 있는 모놀리식~중소 규모 서비스에서 "결제/정산 같은 단계형 작업의 신뢰성"만 올리고 싶을 때 가성비가 가장 좋아 보인다. 운영 컴포넌트를 안 늘려도 되는 게 제일 큰 메리트다.

정리

한 줄 요약: 이미 쓰는 Postgres 트랜잭션을 세이브 포인트 삼아, 중간에 죽어도 이어서 실행되는 워크플로우를 별도 인프라 없이 만든다.

누가 언제 쓰면 좋냐면 — 결제/주문/정산처럼 "중간에 끊기면 곤란한" 다단계 작업이 있고, Temporal 클러스터까지 띄우기는 부담스러운 팀. 반대로 초당 수천 건 워크플로우가 도는 대규모거나, 이미 워크플로우 엔진 운영 노하우가 있다면 굳이 갈아탈 이유는 없다. 도입하더라도 스텝 멱등성 설계와 DB 부하 모니터링은 처음부터 깔고 가야 나중에 안 운다.

참고 자료

※ 라이브러리 API와 버전 관리 정책은 빠르게 바뀔 수 있으니 도입 전 공식 문서로 최신 내용 확인 필요.

사진: Surface / Unsplash

728x90