Ⅰ. Hatchet 기술이란?
왜 필요한가 — 우리는 이미 큐를 4개 운영 중이다
웬만한 SaaS 백엔드를 1년만 굴려보면 같은 풍경이 펼쳐집니다. 이메일 발송은 Python에서 Celery+Redis로, 이미지 리사이즈는 Node 쪽 BullMQ로, 결제 webhook 재시도는 SQS+Lambda로, 야간 배치는 cron 컨테이너로 각각 돌고 있습니다. 큐가 4개, 대시보드가 4개, 죽었을 때 재시도하는 방법이 4개, "지금 큐에 뭐 쌓였지?"라는 질문에 답하는 방법도 4개입니다. 거기에 워크플로우(여러 잡을 순서대로 묶고 실패 시 보상하기)가 필요해지면, Airflow/Step Functions/Temporal까지 다섯 번째 시스템이 들어옵니다.
| 기존 방식 | 손으로 하면 | 현실의 비용 |
| Celery + Redis/RabbitMQ | Python 전용, 결과 백엔드 분리, 재시도는 데코레이터 | 언어 락인, Redis OOM·visibility timeout 함정, "잃어버린 잡"이 남 |
| BullMQ + Redis | Node 전용, Redis Streams 기반 | Python·Go 워커는 끼어들 수 없음, 멀티 언어 시 두 번째 큐 필연 |
| SQS + Lambda | 매니지드, 무한 확장 | 15분 실행 한도, 워크플로우는 Step Functions로 다시, 로컬 개발 어려움 |
| Sidekiq | Ruby 전용 사실상의 표준 | Pro/Enterprise 라이선스, Ruby 외엔 사용 불가 |
| Airflow / Step Functions | DAG 워크플로우 1급 | "한 잡 1초 처리"용으로는 과체중, 스케줄러 락 경합·UI 무거움 |
| Temporal | Durable execution 1급, 장기 실행 워크플로우 강함 | Cassandra/MySQL 의존, 운영 복잡도 큼(615 Restate 글 참조) |
| cron 컨테이너 + DB 플래그 | "가장 단순"하다는 환상 | 중복 실행, 워커 죽으면 잡 영구 정지, 가시성 0 |
모두 같은 본질을 다른 방식으로 풉니다. 그러나 운영자 입장에서 진짜 비용은 "백오피스 시스템이 5개"라는 점입니다. 잡이 안 돌면 어디부터 봐야 할지 슬랙에 던지는 질문이 매주 같은 모양으로 반복됩니다. Hatchet의 출발점은 단순합니다 — 이미 운영 중인 Postgres 한 대에 큐와 워크플로우를 같이 얹고, 워커는 어떤 언어로든 같은 잡을 받게 하자.
기술 정의
Hatchet은 PostgreSQL을 단일 소스로 사용하는 분산 백그라운드 태스크 큐 겸 워크플로우 엔진입니다(MIT 라이선스, hatchet-dev/hatchet, 미국 Hatchet Inc., 2024~). Go로 작성된 엔진(api·engine)이 잡 디스패치·스케줄·상태 머신을 담당하고, Python·TypeScript·Go SDK가 워커와 클라이언트를 제공합니다. 잡은 단일 함수일 수도, 여러 step의 DAG 워크플로우일 수도 있으며, 호출은 RPC 한 번·cron·외부 이벤트(JSON payload)·다른 워크플로우의 결과 등 4가지 방식으로 트리거됩니다. at-least-once 전달, 재시도·timeout·rate limit·concurrency limit·streaming 결과·durable execution이 1급 시민이며, 모든 잡 상태는 Postgres 테이블에 영속화되어 SQL로 직접 감사할 수 있습니다.
Ⅱ. Hatchet 기술 특징
| 특징 | 의미와 실전 효과 |
| Postgres 단일 의존 | Redis·Cassandra·Kafka 추가 없음. RDS·Supabase·셀프호스트 Postgres 어디서든 동작. 백업·복제·관측이 기존 DB 운영과 동일. |
| 언어 중립 워커 | Python·TypeScript·Go SDK가 같은 워크플로우를 스텝 단위로 나눠 실행. Step1=Python, Step2=Go도 자연스럽게 가능. |
| Push-based dispatch | 워커가 polling하지 않고 gRPC long-stream으로 잡을 push 받음. 빈 큐일 때 DB 부하 0에 수렴. |
| Durable execution | step 결과·체크포인트가 Postgres에 영속. 워커가 죽어도 다른 워커가 다음 step부터 이어 받음. |
| Rate limit / concurrency | "이 사용자당 1초에 5건"·"이 워크플로우는 동시 10건"을 데코레이터로 선언. 토큰 버킷이 엔진에서 강제됨. |
| Cron + 이벤트 트리거 | Crontab·event payload(user.created 등) 두 트리거가 1급. cron 컨테이너 별도 운영 불필요. |
| DAG 워크플로우 | step 간 의존 관계 선언, 부모 결과를 자식 입력으로 자동 전달. fan-out/fan-in과 조건부 분기 모두 지원. |
| Streaming 결과 | 스텝이 부분 결과를 yield하면 클라이언트가 SSE/gRPC stream으로 실시간 수신. LLM 토큰 스트리밍·진행률 바에 적합. |
| 웹 대시보드 | 실시간 워크플로우 그래프·재시도 횟수·실행 시간·로그·재실행 버튼이 한 화면. Celery flower의 상위호환. |
| OSS + 매니지드 | 코드는 MIT, Hatchet Cloud는 매니지드 옵션. 셀프호스트와 SaaS 사이 마이그레이션이 양방향(같은 SDK). |
Ⅲ. Hatchet 기술 동작방식
ⅰ. 구성 요소
| 컴포넌트 | 위치 / 구현 | 역할 |
| API server | Go, REST + gRPC | 잡 등록·조회, 토큰 발급, 워크플로우 정의 동기화 |
| Engine | Go, 다중 컨트롤러 | 스케줄러·디스패처·재시도·rate limit·heartbeat 모니터를 분리된 controller로 운영 |
| PostgreSQL | 14+, pg_partman 권장 | 단일 소스. SKIP LOCKED·advisory lock으로 큐 잠금 |
| Worker | Python/TS/Go SDK, 사용자 프로세스 | 엔진과 long-stream gRPC 유지. 잡을 받아 실행하고 결과·heartbeat 송신 |
| Dashboard | React + Vite | 실시간 워크플로우 시각화, 재실행, 로그·이벤트 검색 |
| RabbitMQ(선택) | 단일 인스턴스 | 초고처리량 모드용 컨트롤러 fan-out 채널. 작은 운영에선 비활성화 가능 |
| Prometheus exporter | 엔진 내장 | queue depth·step latency·worker count 지표를 /metrics로 노출 |
ⅱ. 데이터 흐름 — 잡 하나가 클라이언트 호출부터 결과까지
- 클라이언트가 SDK로 workflow.run(input) 호출 → API server가 workflow_runs·step_runs 행 INSERT.
- Engine 스케줄러가 SELECT ... FOR UPDATE SKIP LOCKED로 다음 실행 가능한 step을 집고, 적합한 워커를 매칭.
- 해당 워커의 long-stream gRPC로 step 페이로드를 push. 워커는 함수를 실행하고 진행 상황을 heartbeat로 보고.
- 워커가 결과/예외 반환 → engine이 UPDATE step_runs SET status='SUCCEEDED', 의존하는 다음 step을 enqueue.
- 실패 시 retry 정책 평가 → 백오프 지연 후 재 enqueue. 최대 횟수 초과 시 FAILED로 마킹.
- 모든 step 종료 시 워크플로우 결과 확정. 클라이언트는 .result() await 또는 stream subscription으로 수신.
- 대시보드는 Postgres NOTIFY 기반으로 변화를 구독해 실시간 그래프 갱신.
Ⅳ. Hatchet 기술 구성 및 흐름도
+-------------------+
| Client (SDK/CLI) |
+---------+---------+
| gRPC
v
+-------------------+--------------------+
| Hatchet API + Engine |
| |
| +-----------+ +-------------------+ |
| | Scheduler |-->| Dispatcher | |
| | (SKIP | | (long-stream gRPC)| |
| | LOCKED) | +---------+---------+ |
| +-----+-----+ | |
| | | |
| v v |
| PostgreSQL Worker Pool |
| (single source) (Py / TS / Go) |
+--------+-----------------------+-------+
^ |
| status / results |
+-----------------------+
+----------------+ +-----------------+ +--------------+
| Cron Trigger | | Event Trigger | | Dashboard |
| (engine inner) | | (POST /events) | | (React, NTFY)|
+----------------+ +-----------------+ +--------------+
단계별 설명
| # | 단계 | 설명 |
| 1 | 워크플로우 등록 | 워커 부팅 시 SDK가 워크플로우 정의(이름·step·재시도·rate limit)를 API에 동기화. Postgres에 spec 저장. |
| 2 | 트리거 발생 | RPC 호출 / cron 만기 / 이벤트 publish 중 하나로 workflow_runs 행이 PENDING으로 INSERT. |
| 3 | 스케줄러 picking | Engine이 FOR UPDATE SKIP LOCKED로 다음 실행 가능 step을 집음. rate limit·concurrency 위반이면 보류. |
| 4 | 워커 디스패치 | 매칭된 워커의 gRPC stream으로 step 페이로드 push. 워커는 즉시 RUNNING으로 마킹. |
| 5 | Heartbeat 추적 | 워커가 N초마다 heartbeat 송신. 누락 시 step을 다른 워커로 재할당(at-least-once). |
| 6 | 결과 영속화 | 반환값을 step_run_outputs에 JSONB로 저장. 의존 step의 입력으로 자동 전달. |
| 7 | 완료 / 실패 처리 | DAG 종료 시 결과 통지(NOTIFY). 실패는 retry 백오프 후 재 enqueue 또는 FAILED 확정. |
Ⅴ. Hatchet 기술 설치 방법
ⅰ. Docker Compose (권장 셀프호스트)
# 공식 quickstart compose
git clone https://github.com/hatchet-dev/hatchet.git
cd hatchet/hack/dev
docker compose up -d
# 헬스체크
curl http://localhost:8080/api/ready
# 대시보드
open http://localhost:8080
# 초기 로그인: admin@example.com / Admin123!!
이 compose는 Postgres·RabbitMQ·API·engine·dashboard·migrate 컨테이너를 한 번에 띄웁니다. 개발/POC 용으로 충분하며, 운영에서는 Postgres만 외부로 분리하고 나머지를 Helm/K8s로 옮기는 패턴이 표준입니다.
ⅱ. Helm (운영 K8s)
helm repo add hatchet https://hatchet-dev.github.io/hatchet-charts
helm repo update
# values.yaml 핵심 항목
# postgres:
# external:
# uri: postgres://hatchet:***@db.internal:5432/hatchet
# rabbitmq:
# enabled: true
# api:
# replicas: 2
# engine:
# replicas: 2
helm install hatchet hatchet/hatchet-stack -n hatchet --create-namespace -f values.yaml
ⅲ. SDK 설치 (워커/클라이언트)
# Python
pip install hatchet-sdk
# TypeScript
npm install @hatchet-dev/typescript-sdk
# Go
go get github.com/hatchet-dev/hatchet/pkg/client
# 공통 환경변수
export HATCHET_CLIENT_TOKEN="ey..." # 대시보드 → API tokens 에서 발급
export HATCHET_CLIENT_TLS_STRATEGY=none # 셀프호스트 평문일 때
Ⅵ. Hatchet 기술 사용 방법
ⅰ. 첫 워크플로우 — 이메일 발송 단일 스텝(Python)
from hatchet_sdk import Hatchet, Context
hatchet = Hatchet()
@hatchet.workflow(on_events=["user:signed_up"])
class SendWelcome:
@hatchet.step(retries=3, timeout="30s")
def send(self, ctx: Context) -> dict:
user = ctx.workflow_input()["user"]
# 실제 메일 발송...
return {"sent_to": user["email"]}
worker = hatchet.worker("welcome-worker", max_runs=10)
worker.register_workflow(SendWelcome())
worker.start()
이벤트 트리거가 1급이므로, API 쪽에서는 hatchet.event.push("user:signed_up", {"user": {...}}) 한 줄이면 끝입니다. 워커가 죽었다 살아나도 push된 이벤트는 큐에 남아 다음 워커가 받습니다.
ⅱ. DAG 워크플로우 — 영수증 OCR → 분류 → 통지
@hatchet.workflow(on_events=["receipt:uploaded"])
class ProcessReceipt:
@hatchet.step()
def ocr(self, ctx: Context) -> dict:
url = ctx.workflow_input()["url"]
return {"text": run_ocr(url)}
@hatchet.step(parents=["ocr"], retries=2)
def classify(self, ctx: Context) -> dict:
text = ctx.step_output("ocr")["text"]
return {"category": classify_llm(text)}
@hatchet.step(parents=["classify"])
def notify(self, ctx: Context) -> dict:
cat = ctx.step_output("classify")["category"]
slack_post(f"#fin {cat} 영수증 한 건 도착")
return {"posted": True}
parents=["ocr"] 한 줄로 의존 관계가 만들어집니다. 별도 DAG YAML이 없고, 부모 step의 결과는 ctx.step_output()로 전달됩니다. 중간 step 하나만 죽으면 그 step만 재시도되고 OCR은 다시 돌지 않습니다.
ⅲ. Rate limit / Concurrency / Cron
from hatchet_sdk import RateLimit, ConcurrencyLimitStrategy
@hatchet.workflow(
on_crons=["0 * * * *"], # 매시 정각
schedule_timeout="5m",
)
class HourlyReport:
@hatchet.concurrency(
max_runs=5,
limit_strategy=ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
expression="input.tenant_id", # 테넌트별 동시 5건
)
@hatchet.step(rate_limits=[RateLimit(static_key="openai", units=1)])
def render(self, ctx: Context) -> dict:
...
ⅳ. 운영 시 고려사항
- Postgres 사이즈: 잡 100만/일 기준 step_runs 테이블이 빠르게 커짐. pg_partman으로 일자 파티셔닝 + 30일 보존이 안전.
- 커넥션 풀: API/engine/모든 워커가 동시에 Postgres에 붙음. PgBouncer transaction pooling 모드 권장. max_connections 200 이상.
- 잡 페이로드 크기: workflow_input은 JSONB. 1MB 이상 바이너리는 S3에 두고 키만 전달.
- 토큰 회수: 워커별로 다른 token을 발급하고, 폐기 시 대시보드에서 revoke. 토큰을 컨테이너 환경변수가 아닌 K8s Secret으로 주입.
- 관측: /metrics를 Prometheus로 긁고 queue depth·dispatch latency·worker heartbeat miss를 알람. VictoriaMetrics(637) 조합이 가볍다.
- 재시도 멱등성: at-least-once이므로 step 함수는 idempotent해야 함. 외부 호출은 idempotency-key를 명시.
Ⅶ. Hatchet 기술 자주 쓰는 명령어
ⅰ. CLI / 운영
| 명령 | 용도 |
| docker compose up -d | 스택 전체 기동(Postgres·API·engine·dashboard) |
| curl localhost:8080/api/ready | API 헬스체크 |
| curl localhost:8080/api/v1/metrics | Prometheus 지표 노출 |
| docker compose logs -f engine | 엔진(스케줄러·디스패처) 로그 추적 |
| docker exec -it postgres psql -U hatchet | DB 직접 조회 |
| SELECT status, count(*) FROM step_runs GROUP BY 1; | 상태별 잡 카운트(가장 빠른 디버깅) |
| helm upgrade hatchet hatchet/hatchet-stack -f values.yaml | K8s 운영 업그레이드 |
ⅱ. SDK (Python) — 자주 쓰는 호출
# 워크플로우 트리거
ref = hatchet.client.admin.run_workflow("ProcessReceipt", {"url": "s3://..."})
result = ref.sync_result() # 블로킹 대기
# 이벤트 푸시(매칭되는 워크플로우 모두 트리거)
hatchet.event.push("receipt:uploaded", {"url": "s3://..."})
# 진행 상황 스트리밍
async for chunk in ref.stream():
print(chunk)
# 잡 재실행(대시보드의 replay 버튼 동등)
hatchet.client.admin.replay_workflow_run(ref.workflow_run_id)
# Cron 스케줄 추가/삭제(동적)
hatchet.client.admin.create_cron("HourlyReport", expression="*/15 * * * *")
사례 — 멀티 언어 SaaS의 단일 큐 통합
시나리오: API는 TypeScript(Node), 머신러닝은 Python, 결제 정산 배치는 Go로 짜인 30명 규모 SaaS. 현재 BullMQ + Celery + cron 컨테이너 + GitHub Actions 야간 잡이 따로 돌고 있음.
설계: RDS Postgres에 Hatchet 스택을 얹고, 세 언어 워커를 같은 워크플로우 정의에 등록. 결제 정산은 Go step, ML 추론은 Python step, 알림 발송은 TS step의 DAG 한 개로 묶음. cron은 Hatchet에 위임, GitHub Actions는 그대로 코드 빌드만 담당.
효과: 큐 4개 → 1개. "지금 안 돌고 있는 잡이 뭐냐"는 질문에 대시보드 한 화면으로 답이 나옴. 야간 알람 호출 빈도 60% 감소.
Ⅷ. Hatchet 기술 활용방안
ⅰ. 대안 기술 비교
| 대안 | 강점 | Hatchet 대비 한계 |
| Celery + Redis | Python 생태계 사실상의 표준, 풍부한 플러그인 | Python 락인, Redis OOM/visibility timeout 함정, DAG·rate limit 약함 |
| BullMQ | Node 생태계 표준, Redis Streams 기반의 단순함 | Node 락인, 멀티 언어 워커 부재, 워크플로우 1급 아님 |
| SQS + Lambda | 매니지드, 무한 확장, 운영 부담 0 | 15분 한도, AWS 락인, Step Functions 별도, 로컬 개발 어려움 |
| Temporal | Durable execution 1급, 성숙한 워크플로우 모델, 다언어 SDK | Cassandra/MySQL 의존, 운영 무거움, 단순 잡 큐로는 과체중 |
| Inngest | 이벤트 1급, 서버리스 친화, 좋은 DX | 매니지드 중심, 셀프호스트 OSS는 더 어림, 가격 모델 종속 |
| River (Go) | Postgres 큐, Go 단일 바이너리, 매우 가벼움 | Go 전용, 워크플로우/DAG·rate limit·UI는 자체 추가 필요 |
| Airflow | 데이터 파이프라인의 표준, 풍부한 operator | 초당 잡·실시간 트리거에 약함, 스케줄러 락 경합 |
| Restate (615) | 임베디드 Durable Execution, 단일 바이너리 | 대시보드·rate limit·cron의 1급 시민화 정도가 다름. 사용 결이 다름. |
ⅱ. 언제 쓰면 안 되는가
- 잡 < 100건/일, 워커 1대: cron + DB 플래그 한 줄로 충분. Hatchet 스택 운영 비용이 잡 처리 비용보다 큼.
- 밀리초급 fan-out + 결과 미사용: Kafka·NATS 같은 브로커가 본래 목적. Hatchet은 "결과를 추적"하는 잡 큐.
- 대용량 스트림 처리(초당 수십만 이벤트, ETL): Flink·RisingWave(584)·AutoMQ(581)가 적합. Hatchet은 "함수 실행" 추상화.
- 강한 결정론·재현성이 우선: Temporal/Restate의 정형화된 모델이 더 엄격. Hatchet은 실용 위주.
- Postgres가 운영상 금지된 환경: MySQL/Mongo만 허용된 회사라면 Postgres 추가 운영 비용을 별도 검토.
- 매우 긴 외부 동기 호출(>1시간): step 단위로 잘게 쪼개거나 외부 폴링으로 분리. 한 step이 너무 길면 워커 재시작 시 영향이 큼.
Ⅸ. 주의할 점 & 트러블슈팅
| 증상 | 원인 | 해결 |
| 잡이 PENDING에 멈춘 채 워커가 못 받음 | 워커가 register한 워크플로우 이름·버전이 트리거 측과 불일치 | 대시보드의 Workers 탭에서 register된 워크플로우 목록 확인. 동일 이름으로 재등록 후 워커 재시작. |
| 같은 step이 두 번 실행됨 | at-least-once 보장의 정상 케이스. 워커 SIGKILL 직후 heartbeat miss로 재할당 | step 함수를 idempotent하게 작성. 외부 API는 Idempotency-Key 헤더로 중복 가드. |
| "too many connections" Postgres 에러 | 워커 수 × 풀 크기 + engine + API 합계가 max_connections 초과 | PgBouncer를 transaction pooling으로 앞에 두기. 워커 SDK의 풀을 5~10으로 제한. |
| step_runs 테이블이 수십 GB로 커짐 | 완료 잡을 보존만 하고 정리 안 함 | pg_partman로 일자 파티셔닝 + 30일 retention. 또는 야간 잡으로 SUCCEEDED 상태 30일 이전 행 삭제. |
| 대시보드가 비어 보임(잡은 도는데) | 대시보드는 tenant 컨텍스트 기반. 다른 tenant의 토큰으로 보고 있음 | 대시보드 우상단 tenant 셀렉터 확인. 환경변수 토큰과 같은 tenant인지 점검. |
| Rate limit이 걸려 잡이 무한 보류 | 정적 키(openai 등) 토큰 충전이 0이거나 새로 등록한 키와 일치하지 않음 | 대시보드 Rate Limits 탭에서 잔량 확인. put_rate_limit API로 충전량/주기 재설정. |
| Cron이 정시에 안 돔 | 엔진 컨테이너 시간대가 UTC인데 표현식을 KST 기준으로 작성 | cron 표현식에 timezone 명시(CRON_TZ=Asia/Seoul). 컨테이너 TZ 환경변수도 점검. |
| 버전 업그레이드 후 워커가 enrich 에러 | SDK 버전 < 엔진 버전. proto 정의 호환성 깨짐 | 엔진과 모든 SDK를 동일 마이너 버전으로 맞춤. 점진적 롤아웃 시 임시로 옛 엔진 버전 유지. |
| 한 잡이 워커 메모리를 누수 | SDK max_runs 설정 없이 장기 워커가 누적 | max_runs=N으로 워커당 동시 잡 한도. K8s에서 resources.limits.memory + OOMKill 후 자동 재시작. |
| Webhook 트리거 늦게 처리 | 이벤트 push가 동기 HTTP인데 대량 부하로 API server에 적체 | API server replicas 증설, 또는 webhook 수신을 가벼운 게이트웨이로 받아 비동기 push로 위임. |
| 로컬 개발에서 워커 연결 안 됨 | HATCHET_CLIENT_TLS_STRATEGY 미설정 + 셀프호스트 평문 | HATCHET_CLIENT_TLS_STRATEGY=none + 호스트가 localhost가 아니라 127.0.0.1인지 확인. WSL/Docker 네트워크 분리 점검. |
'어플리케이션' 카테고리의 다른 글
| VPN·점프호스트의 끝, 제로트러스트 액세스 플랫폼 Teleport (0) | 2026.07.02 |
|---|---|
| Kong의 PG 의존·5초 Reload를 끝낸다, Apache APISIX (0) | 2026.06.30 |
| 구글 검색 성공률 높이는 연산자, 원하는 '알짜 정보'만 타겟팅하여 골라내는 마법의 검색법 (0) | 2026.06.29 |
| Caddy란? 자동 HTTPS 시대의 웹 서버와 리버스 프록시 (0) | 2026.06.05 |
| 웹 자동화의 표준, Selenium에 대해 알아보겠습니다. (0) | 2026.04.29 |