Earendil에서 자체 사용을 위해 Postgres 위에만 구축한 내구성 실행 시스템 Absurd를 지난 다섯 달 동안 프로덕션에서 운영하며 얻은 경험, 바뀐 점, 잘 버틴 설계, 아직 부족한 부분, 그리고 오픈 소스에 대한 생각을 정리합니다.
Armin Ronacher의 생각과 글
작성일: 2026년 4월 04일
약 다섯 달 전, 나는 Earendil에서 우리 스스로 쓰기 위해 만든 내구성 실행 시스템 Absurd에 대해 글을 썼다. 이 시스템은 전적으로 Postgres, 그리고 Postgres만의 위에 올라가 있다. 요점은 단순했다. 내구성 있는 워크플로를 얻기 위해 별도의서비스, 컴파일러 플러그인, 혹은 전체 런타임은 필요하지 않다. 필요한 것은 SQL 파일 하나와 얇은 SDK다.
그 이후 우리는 이것을 프로덕션에서 운영해 왔고, 그 경험이 어땠는지 공유할 가치가 있겠다고 생각했다. 짧게 말하면 이렇다. 설계는 잘 버텼고, 시스템은 다루는 즐거움이 있었으며, 다른 사람들도 대체로 그렇게 느끼는 듯하다.
Absurd는 전적으로 Postgres 내부에 존재하는 내구성 실행 시스템이다. 핵심은 작업 관리, 체크포인트 저장, 이벤트 처리, 그리고 클레임 기반 스케줄링을 위한 저장 프로시저를 정의하는 단일 SQL 파일(absurd.sql)이다. 그 위에는 선택한 언어에서 시스템을 쓰기 편하게 만들어 주는 얇은 SDK들이 올라간다. 현재는 TypeScript, Python, 그리고 실험적인 Go 버전이 있다.
모델은 직관적이다. 작업을 등록하고, 그것을 여러 단계로 분해하며, 각 단계는 체크포인트 역할을 한다. 무엇이든 실패하면 작업은 마지막으로 완료된 단계부터 다시 시도한다. 작업은 잠들 수 있고, 외부 이벤트를 기다릴 수 있으며, 며칠 또는 몇 주 동안 중단 상태로 있을 수 있다. 모든 상태는 Postgres에 저장된다.
전체 소개가 궁금하다면 원래 블로그 글에서 기본 개념을 다룬다. 여기서는 그 이후 우리가 배운 것들을 이야기한다.
지난 다섯 달 동안 이 프로젝트는 여러 차례 릴리스를 거쳤다. 대부분의 변경 사항은 사람들이 실제로 의존하기 시작한 시스템이라면 예상할 수 있는 것들이었다. 강화된 클레임 처리, 망가진 워커를 종료하는 워치독, 데드락 방지, 올바른 리스 관리, 이벤트 경쟁 상태 처리, 그리고 실제 워크로드를 돌릴 때만 드러나는 온갖 예외 상황들이다.
특히 따로 짚고 싶은 몇 가지가 있다.
분해된 단계. 원래 설계에는 ctx.step()만 있었고, 여기에 함수를 넘기면 체크포인트된 결과를 돌려받는 방식이었다. 많은 경우에는 잘 작동하지만, 모든 경우에 충분하지는 않았다. 때로는 다음에 무엇을 할지 결정하기 전에 어떤 단계가 이미 실행되었는지 알아야 한다. 그래서 우리는 beginStep() / completeStep()을 추가했다. 이것은 결과를 확정하기 전에 확인할 수 있는 핸들을 제공한다. 이것은 의도적인 실패와 조건부 로직을 모델링하는 데 매우 유용하다는 것이 드러났다. 특히 “호출 전”과 “호출 후” 형태의 훅 API와 함께 작업할 때는 이것이 필요하다.
작업 결과. 이제는 작업을 생성해 두고, 다른 일을 하다가 나중에 돌아와 그 결과를 가져오거나 기다릴 수 있다. 지나고 보면 당연해 보이지만, 원래 시스템은 순수한 fire-and-forget 방식이었다. 결과를 제대로 들여다볼 수 있게 되면서, 부모 워크플로 안에서 자식 작업을 생성하고 그것들이 끝나기를 기다리는 같은 용도로 Absurd를 사용할 수 있게 되었다. 이것은 에이전트로 디버깅할 때도 특히 유용하다.
absurdctl. 우리는 이것을 제대로 된 CLI 도구로 발전시켰다. 스키마 초기화, 마이그레이션 실행, 큐 생성, 작업 생성, 이벤트 발행, 실패 재시도를 명령줄에서 할 수 있다. uvx로 설치하거나 독립 실행형 바이너리로 설치할 수 있다. 이것은 프로덕션 문제를 디버깅하는 데 매우 큰 도움이 되었다. 뭔가가 멈췄을 때 absurdctl dump-task --task-id=<id>를 실행해 정확히 어디에서 멈췄는지 바로 볼 수 있다는 것은 로그를 뒤지는 것과는 전혀 다른 경험이다.
Habitat. 작업, 실행, 체크포인트, 이벤트를 모니터링하기 위한 웹 대시보드를 제공하는 작은 Go 애플리케이션이다. Postgres에 직접 연결해서 현재 무슨 일이 일어나고 있는지 실시간으로 보여준다. 단순하지만, 사람 입장에서 시스템을 더 즐겁게 만들어 주는 종류의 도구다.
에이전트 통합. Absurd는 원래 에이전트 워크로드를 위해 만들어졌기 때문에, 코딩 에이전트가 absurdctl을 통해 워크플로 상태를 디버깅할 수 있도록 발견하고 사용할 수 있는 번들 스킬을 추가했다. 또한 각 메시지를 체크포인트로 기록함으로써 pi 에이전트 턴을 내구성 있게 만드는 문서화된 패턴도 있다.
내가 가장 만족스러운 부분은 핵심 설계가 그다지 많이 바뀔 필요가 없었다는 점이다. 작업, 단계, 체크포인트, 이벤트, 중단이라는 근본 모델은 여전히 처음과 정확히 같다. 그 주변에 기능을 추가했을 뿐, 기본 추상화를 다시 생각해야 할 정도의 문제는 없었다.
복잡성을 SQL에 두고 SDK는 얇게 유지한 결정도 정말 좋은 선택으로 드러났다. TypeScript SDK는 약 1,400줄이다. Python SDK는 약 1,900줄인데, 이 대부분은 색이 있는 함수 지원의 복잡성에서 비롯된다. 이를 Temporal의 Python SDK 약 170,000줄과 비교해 보라. 이 말은 SDK를 이해하기 쉽고, 디버깅하기 쉽고, 포팅하기 쉽다는 뜻이다. 무언가 잘못되었을 때 오후 한나절이면 SDK 전체를 읽고 그것이 무엇을 하는지 이해할 수 있다.
체크포인트 기반 리플레이 모델도 시간이 지나며 잘 버텼다. 워크플로 함수 전체의 결정적 리플레이를 요구하는 시스템과 달리, Absurd는 단지 캐시된 단계 결과를 불러오고 완료된 작업을 건너뛴다. 즉, 단계 바깥의 코드는 결정적일 필요가 없다. 단계 사이에서 Math.random()이나 datetime.now()를 호출해도 문제없이 동작하는데, 중요한 것은 단계 경계뿐이기 때문이다. 실제로는 이것이 무엇이 안전하고 무엇이 아닌지를 훨씬 쉽게 판단하게 해 준다.
풀 기반 스케줄링도 옳은 선택이었다. 워커는 처리 여력이 생길 때마다 Postgres에서 작업을 가져온다. 코디네이터도 없고, 푸시 메커니즘도 없고, HTTP 콜백도 없다. 덕분에 아주 쉽게 셀프 호스팅할 수 있고, 인프라 수준에서 부하 관리를 따로 고민할 필요도 없다.
적절한 추상화가 durable promise여야 했는지에 대해 몇몇 사람들과 이야기를 나눴다. 매우 매력적인 아이디어이긴 하지만, 실제로 구현해 보면 훨씬 더 복잡해진다는 것이 드러난다. 다만 이론적으로는 더 강력하기도 하다. absurd가 durable promise를 기반으로 하면 어떤 모습일지 알아보려는 시도를 몇 번 해 보았지만, 지금까지는 별다른 진전을 이루지 못했다. 그래도 재미있게 시도해 볼 만한 실험이라고는 생각한다.
주요 사용 사례는 여전히 에이전트 워크플로다. 에이전트는 본질적으로 LLM을 호출하고, 도구 결과를 처리하고, 끝났다고 판단할 때까지 이를 반복하는 루프다. 각 반복은 하나의 단계가 되고, 각 단계의 결과는 체크포인트된다. 프로세스가 7번째 반복에서 죽으면 다시 시작해서 저장소에 있는 1부터 6까지의 반복을 리플레이한 다음 7부터 계속 진행한다.
하지만 우리는 이것이 다른 많은 일에도 유용하다는 것을 알게 되었다. 우리의 모든 cron은 호출에서 미리 생성한 중복 제거 키와 함께 분산 워크플로를 디스패치할 뿐이다. 두 개의 cron 프로세스를 실행하더라도 실제로는 absurd 작업 호출 하나만 트리거된다. 또한 배포를 견뎌야 하는 백그라운드 처리에도 이것을 사용한다. 기본적으로 큐 위에 직접 재시도 및 재개 로직을 얹어 만들어야 할 거의 모든 것에 쓸 수 있다.
Absurd는 의도적으로 최소한에 머무르지만, 보고 싶은 것들은 여전히 있다.
내장 스케줄러는 없다. cron과 비슷한 동작이 필요하다면 직접 스케줄러 루프를 돌리고 멱등성 키를 사용해 중복을 제거해야 한다. 이것도 작동하고, 이를 위한 문서화된 패턴도 있지만, 좀 더 통합된 무언가가 있으면 좋겠다.
푸시 모델도 없다. 모든 것은 풀 방식이다. 웹훅을 받아 작업을 깨우는 HTTP 엔드포인트가 필요하다면 직접 만들어야 한다. 푸시 시스템은 운영하기 더 어렵고 과부하되기 더 쉬우므로 이것이 올바른 기본값이라고 생각하지만, 편리할 경우도 있다. 특히 꽤 많은 에이전트 시스템에서 웹훅이 기본적으로 통합되어 있으면 정말 좋을 것이다. 들어오는 POST 요청에서 깨어나는 식으로 말이다. 나는 이것이 핵심에 들어가기를 원하지는 않지만, absurd 위에 구축되는 좋은 인접 라이브러리의 문제가 될 수는 있어 보인다.
가장 큰 빠진 부분은 아직 파티셔닝을 지원하지 않는다는 점이다. 이건 안타까운데, 데이터 정리가 필요 이상으로 비싸지기 때문이다. 이론적으로 파티션 지원 자체는 꽤 단순할 수 있다. 주 단위 파티션을 두고 만료되면 분리한 뒤 삭제하면 된다. 실제로 그것을 가로막는 유일한 문제는 Postgres가 그것을 수행할 편리한 방법을 제공하지 않는다는 점이다.
어려운 부분은 파티셔닝 자체가 아니라 실제 워크로드에서의 파티션 생애주기 관리다. 워커가 expires_at이 파티션이 없는 달에 해당하는 행을 삽입하면 삽입이 실패하고 워크플로가 중단된다. 따라서 절전/재시도를 충분히 감당할 수 있을 만큼 미래의 파티션을 항상 미리 만들어 두는 별도의 유지보수 루프가 필요하고, 이것을 모든 큐에 대해 해야 한다.
삭제 측면에서 안전한 접근은 DETACH PARTITION CONCURRENTLY이지만, 이것을 pg_cron에서 실행하게 만드는 것은 동작하지 않는다. 이것은 트랜잭션 안에서 실행될 수 없는데, pg_cron은 모든 것을 하나의 트랜잭션 안에서 실행하기 때문이다.
해결 불가능한 문제라고는 생각하지 않지만, 아직 좋은 해법을 찾지 못한 문제이며 의견을 듣고 싶다.
이것은 조금 메타적인 지점으로 이어지는데, 에이전트 엔지니어링 시대에 오픈 소스 라이브러리의 의미가 무엇인가 하는 문제다. Durable Execution은 이제 많은 스타트업이 판매하는 무언가다. 반면 에이전트가 그것을 직접 만들어 줄 수도 있고, 사람들은 이제 더 이상 해법을 찾아보지도 않을 수 있다. 좀… 이상하다.
나는 내구성 실행 라이브러리가 회사를 먹여 살릴 수 있다고는 생각하지 않는다. 정말 그렇게 생각하지 않는다. 반면 이것은 상업적 이해관계에서 벗어난 좋은 오픈 소스 프로젝트가 되기에는 딱 복잡할 정도의 문제라고는 생각한다. 특히 UI와 디버깅을 위한 좋은 DX 측면에서 어느 정도 생태계가 필요하고, 그런 것은 일회성 구현으로 얻기 어렵다.
아직 이 문제를 완전히 풀어낸 것은 아니지만, 몇 달 전보다는 이미 훨씬 더 쓰기 좋아졌다.
Absurd를 사용하고 있거나, 도입을 생각하고 있거나, 인접한 아이디어를 만들고 있다면 여러분의 피드백을 듣고 싶다. 버그 리포트, 거친 부분, 설계 비평, 기여 모두 매우 환영한다. 누군가가 다른 각도에서 이 프로젝트를 건드릴 때마다 더 나아졌기 때문이다.
이 글은 ai 및 announcements 태그가 달려 있다.
© 저작권 2026 Armin Ronacher.
콘텐츠는 Creative Commons Attribution-NonCommercial 4.0 International License에 따라 라이선스된다.
메일, bluesky, x, 또는 github으로 연락할 수 있다.
더 많은 정보: imprint&AI transparency. atom / RSS로 구독할 수 있다.
색상 구성표: 자동 , 밝게 , 어둡게 .