<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Workflow on Yarang's Tech Lair</title><link>https://blog.agentthread.dev/ko/tags/workflow/</link><description>Recent content in Workflow on Yarang's Tech Lair</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Sat, 30 May 2026 09:00:30 +0900</lastBuildDate><atom:link href="https://blog.agentthread.dev/ko/tags/workflow/index.xml" rel="self" type="application/rss+xml"/><item><title>SQLite 기반 영구적 워크플로우 설계: 'SQLite is all you need' 실전 가이드</title><link>https://blog.agentthread.dev/ko/post/sqlite-%EA%B8%B0%EB%B0%98-%EC%98%81%EA%B5%AC%EC%A0%81-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%84%A4%EA%B3%84-sqlite-is-all-you-need-%EC%8B%A4%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/</link><pubDate>Sat, 30 May 2026 09:00:30 +0900</pubDate><guid>https://blog.agentthread.dev/ko/post/sqlite-%EA%B8%B0%EB%B0%98-%EC%98%81%EA%B5%AC%EC%A0%81-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%84%A4%EA%B3%84-sqlite-is-all-you-need-%EC%8B%A4%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/</guid><description>&lt;h1 id="sqlite로-워크플로우를-지속-가능하게-만드는-방법"&gt;SQLite로 워크플로우를 지속 가능하게 만드는 방법
&lt;/h1&gt;&lt;p&gt;최근 Hacker News에서 &amp;ldquo;SQLite is all you need for durable workflows&amp;quot;라는 글이 큰 화제가 되었습니다. 분산 시스템을 설계할 때 Kafka, Redis, 복잡한 클러스터링을 떠올리기 쉽지만, 정작 우리가 해결하려는 핵심 문제인 &amp;lsquo;상태 유지(State Management)&amp;lsquo;와 &amp;lsquo;재시작 내성(Resiliency)&amp;lsquo;은 단일 파일 데이터베이스인 SQLite만으로도 충분히 해결할 수 있다는 주장입니다.&lt;/p&gt;
&lt;p&gt;저희 팀의 &amp;lsquo;ZeroClaw&amp;rsquo;나 &amp;lsquo;Discord Decision MCP&amp;rsquo;와 같은 에이전트 시스템을 개발하면서, 복잡한 메시지 큐를 도입하기 전에 SQLite의 가벼우면서도 강력한 트랜잭션 기능을 활용해 워크플로우 엔진을 직접 구현해 보았습니다. 이 글에서는 그 핵심 아키텍처와 실제 코드 예제를 공유합니다.&lt;/p&gt;
&lt;h2 id="1-영구적-워크플로우durable-workflow란"&gt;1. 영구적 워크플로우(Durable Workflow)란?
&lt;/h2&gt;&lt;p&gt;워크플로우가 &amp;lsquo;영구적&amp;rsquo;이다 한다는 것은, 프로그램이 죽거나 서버가 재시작되더라도 중단된 지점부터 다시 시작할 수 있음을 의미합니다. 이를 위해 우리는 다음 두 가지를 보장해야 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;상태 저장(Snapshotting):&lt;/strong&gt; 현재 진행 상황을 지속적으로 저장해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정확히 한 번 실행(Exactly-Once Semantics):&lt;/strong&gt; 재시작 시 중복 실행을 방지해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="2-아키텍처-설계-sqlite를-큐와-상태-저장소로-사용하기"&gt;2. 아키텍처 설계: SQLite를 큐와 상태 저장소로 사용하기
&lt;/h2&gt;&lt;p&gt;SQLite는 &lt;code&gt;WAL(Write-Ahead Logging)&lt;/code&gt; 모드와 고급 잠금 메커니즘 덕분에 동시성 환경에서도 놀라운 성능을 냅니다. 워크플로우 엔진은 다음과 같은 테이블 구조를 기반으로 동작합니다.&lt;/p&gt;
&lt;h3 id="데이터베이스-스키마"&gt;데이터베이스 스키마
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 작업의 현재 상태를 저장하는 테이블
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; workflows (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id TEXT &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; status TEXT &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;, &lt;span style="color:#75715e"&gt;-- pending, running, completed, failed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; current_step INTEGER &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; payload TEXT, &lt;span style="color:#75715e"&gt;-- JSON 형태의 입력 데이터
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; updated_at INTEGER &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 실행해야 할 작업 목록 (큐)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; jobs (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id INTEGER &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt; AUTOINCREMENT,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; workflow_id TEXT &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_step INTEGER &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; run_at INTEGER, &lt;span style="color:#75715e"&gt;-- 예약 실행 시간 (NULL 즉시 실행)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;FOREIGN&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt;(workflow_id) &lt;span style="color:#66d9ef"&gt;REFERENCES&lt;/span&gt; workflows(id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INDEX&lt;/span&gt; idx_jobs_run_at &lt;span style="color:#66d9ef"&gt;ON&lt;/span&gt; jobs(run_at);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="rust-구현-예제"&gt;Rust 구현 예제
&lt;/h3&gt;&lt;p&gt;Rust의 &lt;code&gt;rusqlite&lt;/code&gt; 라이브러리를 사용하여 간단한 워크플로우 스케줄러를 작성해 보겠습니다. 이 코드는 &amp;lsquo;ZeroClaw&amp;rsquo; 런타임의 가벼운 작업 스케줄링 로직에서 영감을 받았습니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;use&lt;/span&gt; rusqlite::{Connection, Result, params};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;use&lt;/span&gt; std::time::{Duration, SystemTime};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;() -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;()&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 1. 메모리 또는 파일 DB 연결 (WAL 모드 활성화 권장)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; conn &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Connection::open(&lt;span style="color:#e6db74"&gt;&amp;#34;workflow.db&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; conn.execute_batch(&lt;span style="color:#e6db74"&gt;&amp;#34;PRAGMA journal_mode=WAL;&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 2. 워크플로우 생성 (초기 상태)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; workflow_id &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;order-12345&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; insert_workflow(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; conn, workflow_id, &lt;span style="color:#e6db74"&gt;&amp;#34;{&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;item&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;: &lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;keyboard&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 3. 작업 예약 (결제 단계)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; enqueue_job(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; conn, workflow_id, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, None)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 4. 워커 루프 (실무에서는 데몬화)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;loop&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;match&lt;/span&gt; fetch_next_job(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; conn)&lt;span style="color:#f92672"&gt;?&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Some(job) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;println!&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Processing job &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; for workflow &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, job.target_step, job.workflow_id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 비즈니스 로직 실행 (예: LLM 호출, 외부 API 요청)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// simulate_work();
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 성공 시 다음 스텝 예약 또는 완료 처리
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; complete_step(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; conn, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;job.workflow_id, job.target_step)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; None &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;println!&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;No jobs, sleeping...&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; std::thread::sleep(Duration::from_secs(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 작업을 큐에 넣고, 워크플로우 상태를 &amp;#39;pending&amp;#39;으로 업데이트하는 트랜잭션
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;enqueue_job&lt;/span&gt;(conn: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;mut&lt;/span&gt; Connection, workflow_id: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;, step: &lt;span style="color:#66d9ef"&gt;i32&lt;/span&gt;, delay: Option&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;SystemTime&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;) -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;()&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; tx &lt;span style="color:#f92672"&gt;=&lt;/span&gt; conn.transaction()&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.execute(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;INSERT INTO jobs (workflow_id, target_step, run_at) VALUES (?1, ?2, ?3)&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;params!&lt;/span&gt;[workflow_id, step, delay.and_then(&lt;span style="color:#f92672"&gt;|&lt;/span&gt;t&lt;span style="color:#f92672"&gt;|&lt;/span&gt; Some(t.duration_since(SystemTime::&lt;span style="color:#66d9ef"&gt;UNIX_EPOCH&lt;/span&gt;).unwrap().as_secs()))]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.execute(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UPDATE workflows SET status = &amp;#39;pending&amp;#39;, updated_at = ?1 WHERE id = ?2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;params!&lt;/span&gt;[SystemTime::now().duration_since(SystemTime::&lt;span style="color:#66d9ef"&gt;UNIX_EPOCH&lt;/span&gt;).unwrap().as_secs(), workflow_id]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.commit()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 다음 처리할 작업을 가져오는 함수 (Locking Read)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetch_next_job&lt;/span&gt;(conn: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;mut&lt;/span&gt; Connection) -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Option&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Job&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// UPDATE 문을 통해 행을 잠그고 반환하는 기법 (UPDATE RETURNING)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// SQLite 3.35.0 이상 지원
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; stmt &lt;span style="color:#f92672"&gt;=&lt;/span&gt; conn.prepare(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UPDATE jobs 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SET workflow_id = workflow_id || &amp;#39;:processing&amp;#39; -- 임시 상태 표시 (실제로는 status 컬럼 추천)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WHERE rowid IN (
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SELECT rowid FROM jobs 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WHERE run_at IS NULL OR run_at &amp;lt;= strftime(&amp;#39;%s&amp;#39;, &amp;#39;now&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; LIMIT 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; RETURNING id, workflow_id, target_step;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; rows &lt;span style="color:#f92672"&gt;=&lt;/span&gt; stmt.query(&lt;span style="color:#a6e22e"&gt;params!&lt;/span&gt;[])&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;match&lt;/span&gt; rows.next()&lt;span style="color:#f92672"&gt;?&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Some(row) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; Ok(Some(Job {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id: &lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;.get(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; workflow_id: &lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;.get(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_step: &lt;span style="color:#a6e22e"&gt;row&lt;/span&gt;.get(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&lt;span style="color:#f92672"&gt;?&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; None &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; Ok(None),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 스템 완료 처리
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;complete_step&lt;/span&gt;(conn: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;mut&lt;/span&gt; Connection, workflow_id: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;, step: &lt;span style="color:#66d9ef"&gt;i32&lt;/span&gt;) -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;()&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; tx &lt;span style="color:#f92672"&gt;=&lt;/span&gt; conn.transaction()&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 완료된 작업 삭제
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.execute(&lt;span style="color:#e6db74"&gt;&amp;#34;DELETE FROM jobs WHERE workflow_id = ?1 AND target_step = ?2&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;params!&lt;/span&gt;[workflow_id, step])&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 워크플로우 상태 업데이트 (다음 단계로)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.execute(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UPDATE workflows SET current_step = ?1, updated_at = ?2 WHERE id = ?3&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;params!&lt;/span&gt;[step &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, SystemTime::now().duration_since(SystemTime::&lt;span style="color:#66d9ef"&gt;UNIX_EPOCH&lt;/span&gt;).unwrap().as_secs(), workflow_id]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.commit()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Job&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id: &lt;span style="color:#66d9ef"&gt;i64&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; workflow_id: String,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_step: &lt;span style="color:#66d9ef"&gt;i64&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="3-왜-이-구조가-강력한가"&gt;3. 왜 이 구조가 강력한가?
&lt;/h2&gt;&lt;p&gt;이 접근 방식의 가장 큰 장점은 &lt;strong&gt;관리 부하(Ops Overhead)가 거의 없다는 점&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ZeroClaw와 같은 에이전트 시스템&lt;/strong&gt;에서는 각 에이전트가 자신의 SQLite 파일을 가지고 동작하므로, 중앙 집중식 DB의 병목 현상을 피할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discord MCP&lt;/strong&gt; 같은 봇을 만들 때, 복잡한 Redis 컨테이너를 띄우는 대신 단순히 &lt;code&gt;discord.db&lt;/code&gt; 파일만 백업해두면 모든 대화 기록과 처리 중인 작업을 완벽하게 보호할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="4-결론-필요한-만큼만"&gt;4. 결론: &amp;ldquo;필요한 만큼만&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;SQLite is all you need&amp;quot;라는 제목은 SQLite가 모든 것을 대체할 수 있다는 뜻이 아니라, &lt;strong&gt;대부분의 문제는 SQLite면 충분하다&lt;/strong&gt;는 뜻입니다. 복잡한 분산 시스템을 도입하기 전에, 가장 단순하고 안정적인 파일 기반 데이터베이스로 워크플로우의 신뢰성을 확보해 보는 것은 어떨까요?&lt;/p&gt;
&lt;p&gt;다음 포스트에서는 이 SQLite 워크플로우 엔진을 Kubernetes 환경에서 안정적으로 운영하기 위한 Persisntent Volume 전략에 대해 다루겠습니다.&lt;/p&gt;</description></item><item><title>Postgres 기반 Durable Workflow 구현: DB를 워크플로우 엔진처럼 쓰기</title><link>https://blog.agentthread.dev/ko/post/postgres-%EA%B8%B0%EB%B0%98-durable-workflow-%EA%B5%AC%ED%98%84-db%EB%A5%BC-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%97%94%EC%A7%84%EC%B2%98%EB%9F%BC-%EC%93%B0%EA%B8%B0/</link><pubDate>Fri, 29 May 2026 09:00:36 +0900</pubDate><guid>https://blog.agentthread.dev/ko/post/postgres-%EA%B8%B0%EB%B0%98-durable-workflow-%EA%B5%AC%ED%98%84-db%EB%A5%BC-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%97%94%EC%A7%84%EC%B2%98%EB%9F%BC-%EC%93%B0%EA%B8%B0/</guid><description>&lt;p&gt;최근 빠르고 가벼운 마이크로 서비스 아키텍처를 구축하면서, 외부 의존성을 최소화하는 것이 중요한 과제로 떠올랐습니다. 특히 ZeroClaw 런타임 개발 과정에서 복잡한 상태 머신 관리와 에이전트 간의 통신 신뢰성을 보장하기 위해 Kafka나 Redis 같은 별도의 메시지 큐를 도입하려 했으나, 운영 복잡도가 우려되었습니다.&lt;/p&gt;
&lt;p&gt;이때 눈에 들어온 것이 최근 Hacker News 등에서 화제가 된 &lt;strong&gt;&amp;lsquo;Postgres 기반 Durable Workflow&amp;rsquo;&lt;/strong&gt; 패턴입니다. 단순히 데이터를 저장하는 것을 넘어, 데이터베이스 자체를 워크플로우 엔진처럼 활용하여 상태 유지와 재시작(Retry) 로직을 처리하는 방식입니다. 이번 포스트에서는 이 패턴의 핵심 개념과 ZeroClaw 프로젝트에 적용한 실용적인 구현 방법을 공유합니다.&lt;/p&gt;
&lt;h3 id="왜-postgres인가"&gt;왜 Postgres인가?
&lt;/h3&gt;&lt;p&gt;기존에는 Temporal.io나 Cadence 같은 전문 워크플로우 엔진을 사용하거나, Kafka를 통해 이벤트 소싱을 구현했습니다. 하지만 소규모 팀이나 개인 프로젝트에서는 이러한 인프라를 유지하는 것이 큰 부담이 됩니다.&lt;/p&gt;
&lt;p&gt;PostgreSQL은 이미 대부분의 서비스가 기본 데이터베이스로 채택하고 있습니다. ACID 트랜잭션을 통해 강력한 일관성을 보장하며, &lt;code&gt;SKIP LOCKED&lt;/code&gt; 기능을 통해 분산 락(Claim-based lock)을 구현할 수 있습니다. 즉, &amp;lsquo;이미 우리가 가진 신뢰할 수 있는 저장소&amp;rsquo;를 큐(Queue)로, 스케줄러로, 그리고 상태 저장소로 활용하여 인프라 스택을 단순화할 수 있습니다.&lt;/p&gt;
&lt;h3 id="핵심-아키텍처-테이블을-큐처럼-사용하기"&gt;핵심 아키텍처: 테이블을 큐처럼 사용하기
&lt;/h3&gt;&lt;p&gt;이 패턴의 핵심은 워크플로우의 상태(State)와 실행 대상(Task)을 테이블에 정의하는 것입니다. ZeroClaw의 에이전트 작업 큐를 예로 들어보겠습니다.&lt;/p&gt;
&lt;h4 id="1-상태-테이블-정의-sql"&gt;1. 상태 테이블 정의 (SQL)
&lt;/h4&gt;&lt;p&gt;우리는 작업의 현재 상태와 다음 실행 시점을 저장하기 위해 &lt;code&gt;workflow_jobs&lt;/code&gt; 테이블을 설계했습니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; workflow_jobs (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id SERIAL &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; job_type TEXT &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;, &lt;span style="color:#75715e"&gt;-- 예: &amp;#39;agent_task&amp;#39;, &amp;#39;discord_notify&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; payload JSONB &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;, &lt;span style="color:#75715e"&gt;-- 실행에 필요한 데이터
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; status TEXT &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;pending&amp;#39;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;-- pending, running, succeeded, failed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; claimed_at &lt;span style="color:#66d9ef"&gt;TIMESTAMP&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; run_after &lt;span style="color:#66d9ef"&gt;TIMESTAMP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;CURRENT_TIMESTAMP&lt;/span&gt;, &lt;span style="color:#75715e"&gt;-- 지연 실행 또는 재시작 타임스탬프
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; last_error TEXT,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; attempt_count INT &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_attempts INT &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; created_at &lt;span style="color:#66d9ef"&gt;TIMESTAMP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 상태 기반 인덱싱 (효율적인 조회를 위해)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INDEX&lt;/span&gt; idx_jobs_claim &lt;span style="color:#66d9ef"&gt;ON&lt;/span&gt; workflow_jobs (status, run_after) &lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; status &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;pending&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;여기서 &lt;code&gt;status&lt;/code&gt;와 &lt;code&gt;run_after&lt;/code&gt; 컬럼이 핵심입니다. 이 두 컬럼을 통해 &amp;lsquo;지금 실행해야 할 작업&amp;rsquo;을 즉시 찾아낼 수 있습니다.&lt;/p&gt;
&lt;h4 id="2-작업-예약-및-실행-rust-예제"&gt;2. 작업 예약 및 실행 (Rust 예제)
&lt;/h4&gt;&lt;p&gt;이제 워커(Worker)는 트랜잭션 내에서 작업을 찾고(&lt;code&gt;SELECT&lt;/code&gt;), 소유권을 주장(&lt;code&gt;UPDATE&lt;/code&gt;)한 뒤 실행해야 합니다. 이 과정에서 경쟁 조건(Race Condition)을 방지하기 위해 &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; 구문을 사용합니다.&lt;/p&gt;
&lt;p&gt;다음은 Rust와 &lt;code&gt;sqlx&lt;/code&gt;를 사용한 구현 예제입니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;use&lt;/span&gt; sqlx::{PgPool, Postgres};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;use&lt;/span&gt; sqlx::types::chrono::{Utc, DateTime};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;use&lt;/span&gt; serde_json::Value;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;struct&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Job&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; id: &lt;span style="color:#66d9ef"&gt;i32&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; job_type: String,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; payload: &lt;span style="color:#a6e22e"&gt;Value&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;/// 워커가 실행 루프에서 호출할 함수
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;/// 트랜잭션 내에서 안전하게 작업을 할당받습니다.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;claim_job&lt;/span&gt;(pool: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;PgPool&lt;/span&gt;, worker_id: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;str&lt;/span&gt;) -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Option&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Job&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;, sqlx::Error&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; now &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Utc::now();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; tx &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pool.begin().&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 1. 실행 가능한 작업을 찾고 락을 겁니다.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// SKIP LOCKED: 다른 워커가 이미 락을 건 행은 무시하고 넘어갑니다.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; query &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;r&lt;/span&gt;&lt;span style="color:#e6db74"&gt;#&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SELECT id, job_type, payload 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM workflow_jobs 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WHERE status = &amp;#39;pending&amp;#39; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; AND run_after &amp;lt;= $1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ORDER BY created_at ASC
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FOR UPDATE SKIP LOCKED
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; LIMIT 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FOR UPDATE OF workflow_jobs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;#&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; job: Option&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Job&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; sqlx::query_as::&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;_, Job&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;(query)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(now)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .fetch_optional(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;tx)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; job &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;match&lt;/span&gt; job {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Some(j) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; j,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; None &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.rollback().&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// 할당받을 작업 없음 (Rollback은 불필요하지만 명시적 표현)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Ok(None);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 2. 작업 상태를 &amp;#39;running&amp;#39;으로 변경
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sqlx::query(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UPDATE workflow_jobs SET status = &amp;#39;running&amp;#39;, claimed_at = $1, attempt_count = attempt_count + 1 WHERE id = $2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(Utc::now())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(job.id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .execute(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;mut&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;tx)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tx.commit().&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Ok(Some(job))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;/// 작업 완료 후 상태를 업데이트하는 함수
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;complete_job&lt;/span&gt;(pool: &lt;span style="color:#66d9ef"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;PgPool&lt;/span&gt;, job_id: &lt;span style="color:#66d9ef"&gt;i32&lt;/span&gt;, success: &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;, error_msg: Option&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;String&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;) -&amp;gt; Result&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;(), sqlx::Error&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; status &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; success { &lt;span style="color:#e6db74"&gt;&amp;#34;succeeded&amp;#34;&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; { &lt;span style="color:#e6db74"&gt;&amp;#34;failed&amp;#34;&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 실패 시 지수 백오프(Exponential Backoff)를 고려하여 run_after를 계산할 수 있습니다.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; next_run &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; success {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; None
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 예: 10초 후 재시도 시도 (실제 구현에서는 attempt_count에 따른 지수 증가 로직 필요)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Some(Utc::now() &lt;span style="color:#f92672"&gt;+&lt;/span&gt; chrono::Duration::seconds(&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; success {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sqlx::query(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UPDATE workflow_jobs SET status = $1, run_after = NULL WHERE id = $2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(status)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(job_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .execute(pool)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sqlx::query(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UPDATE workflow_jobs SET status = $1, last_error = $2, run_after = $3 WHERE id = $4&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(status)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(error_msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(next_run)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .bind(job_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .execute(pool)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Ok(())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="3-워커-루프-구성"&gt;3. 워커 루프 구성
&lt;/h4&gt;&lt;p&gt;이제 위 함수를 사용해 무한 루프(또는 Tokio의 &lt;code&gt;tokio::time::interval&lt;/code&gt;)를 돌며 작업을 처리합니다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;worker_loop&lt;/span&gt;(pool: &lt;span style="color:#a6e22e"&gt;PgPool&lt;/span&gt;, worker_id: String) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;loop&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;match&lt;/span&gt; claim_job(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;pool, &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;worker_id).&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Ok(Some(job)) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;println!&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;[&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;] Job acquired: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{:?}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, worker_id, job.job_type);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 실제 비즈니스 로직 실행 (예: LLM 추론, API 호출)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; result &lt;span style="color:#f92672"&gt;=&lt;/span&gt; execute_logic(job.payload.clone()).&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 결과 반영
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; _ &lt;span style="color:#f92672"&gt;=&lt;/span&gt; complete_job(&lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;pool, job.id, result.is_ok(), result.err().map(&lt;span style="color:#f92672"&gt;|&lt;/span&gt;e&lt;span style="color:#f92672"&gt;|&lt;/span&gt; e.to_string())).&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Ok(None) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 처리할 작업이 없으면 잠시 대기 (CPU 점유율 방지)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tokio::time::sleep(tokio::time::Duration::from_millis(&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;)).&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Err(e) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;eprintln!&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;[&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;] Worker error: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, worker_id, e);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tokio::time::sleep(tokio::time::Duration::from_secs(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)).&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="장단점-및-활용-팁"&gt;장단점 및 활용 팁
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;단순성:&lt;/strong&gt; 복잡한 분산 시스템 이론 없이 RDBMS의 트랜잭션만으로 신뢰성을 확보합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 효율성:&lt;/strong&gt; DB만 백업하면 워크플로우 상태도 함께 복구됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;강력한 일관성:&lt;/strong&gt; 메시지 유실(In-flight loss)에 대한 걱정이 없습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;처리량 처리량:&lt;/strong&gt; Postgres는 연결당 처리량에 한계가 있으므로, 초당 수만 건 이상의 처리량이 필요하면 전문 MQ가 낫습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DB 부하:&lt;/strong&gt; 워커가 순환 쿼리(Polling)를 지속적으로 수행하므로, &lt;code&gt;pgbouncer&lt;/code&gt; 등을 통해 연결 풀을 최적화해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="결론"&gt;결론
&lt;/h3&gt;&lt;p&gt;ZeroClaw의 멀티 에이전트 환경에서는 개별 에이전트의 메시지 처리량이 Kafka를 도입할 만큼 높지 않지만, 상태 관리의 신뢰성은 매우 중요했습니다. Postgres 기반 아키텍처를 통해 별도의 인프라 도입 없이 &amp;lsquo;안정적인 상태 관리&amp;rsquo;와 &amp;lsquo;확장 가능한 작업 큐&amp;rsquo;를 모두 달성할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;만약 당신의 프로젝트가 &amp;lsquo;단순함&amp;rsquo;과 &amp;lsquo;신뢰성&amp;rsquo; 사이에서 고민하고 있다면, 굳이 무거운 솔루션을 도입하기 전에 가까운 곳에 있는 Postgres를 워크플로우 엔진으로 활용해 보는 것은 어떨까요?&lt;/p&gt;
&lt;h3 id="참고자료"&gt;참고자료
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://www.hanselminutes.com/890/building-durable-workflows-on-postgres-with-pavel-duchovny" target="_blank" rel="noopener"
 &gt;Building durable workflows on Postgres&lt;/a&gt; (Hacker News 토론)&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE" target="_blank" rel="noopener"
 &gt;SKIP LOCKED와 분산 락 구현&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>