이메일과 웹후크를 위한 간단한 백그라운드 작업 큐 패턴
이메일 전송, 리포트 실행, 웹후크 전달을 재시도·백오프·데드레터 처리로 안정적으로 수행하는 간단한 데이터베이스 기반 백그라운드 작업 큐 패턴을 배우세요.

백그라운드 작업이 필요한 이유(그리고 왜 금방 복잡해지는가)
1~2초 이상 걸릴 수 있는 작업은 사용자 요청 안에서 처리하면 안 됩니다. 이메일 전송, 리포트 생성, 웹후크 전달은 네트워크, 서드파티 서비스, 느린 쿼리에 의존합니다. 때로는 일시 중단되거나 실패하거나 예상보다 오래 걸립니다.
사용자가 기다리는 동안 그런 작업을 하면 차이가 바로 드러납니다. 페이지가 멈추고, "저장" 버튼이 계속 돌며, 요청이 타임아웃납니다. 재시트가 잘못된 곳에서 발생할 수도 있습니다. 사용자가 새로고침하거나 로드밸런서가 재시도하거나 프론트엔드가 다시 제출하면 중복 이메일, 중복 웹후크 호출, 또는 서로 경쟁하는 두 건의 리포트 실행이 생길 수 있습니다.
백그라운드 작업은 요청을 작고 예측 가능하게 유지해 이 문제를 해결합니다: 작업을 수락하고 나중에 실행할 작업을 기록한 뒤 빠르게 응답합니다. 작업은 요청 밖에서, 여러분이 제어하는 규칙으로 실행됩니다.
문제는 신뢰성입니다. 작업이 요청 경로 밖으로 이동하면 다음과 같은 질문에 답해야 합니다:
- 이메일 제공자가 3분 동안 다운되면 어떻게 하나요?
- 웹후크 엔드포인트가 500을 반환하거나 타임아웃이면?
- 작업이 두 번 실행되면?
- 사용자가 불평하기 전에 걸린 작업을 어떻게 감지하나?
많은 팀이 "무거운 인프라"를 추가해 대응합니다: 메시지 브로커, 별도 워커 플릿, 대시보드, 알림, 플레이북 등. 이런 도구는 진짜로 필요할 때 유용하지만 새로운 구성요소와 실패 모드를 추가합니다.
더 나은 시작 목표는 단순함입니다: 이미 가진 부품으로 신뢰성 있는 작업을 만드는 것. 대부분의 제품에서는 데이터베이스 기반 큐와 작은 워커 프로세스면 충분합니다. 명확한 재시도와 백오프 전략, 지속적으로 실패하는 작업을 위한 데드레터 패턴을 추가하면 복잡한 플랫폼으로 즉시 커밋하지 않고도 예측 가능한 동작을 얻을 수 있습니다.
Koder.ai 같은 채팅 기반 도구로 빠르게 개발하더라도 이 분리는 중요합니다. 사용자는 지금 빠른 응답을 받아야 하고, 시스템은 느리고 실패하기 쉬운 작업을 백그라운드에서 안전하게 마무리해야 합니다.
큐를 간단히 설명하면
큐는 작업을 기다리는 줄입니다. 사용자 요청 동안 느리거나 신뢰할 수 없는 작업(이메일 전송, 리포트 생성, 웹후크 호출)을 수행하는 대신 작은 레코드를 큐에 넣고 빠르게 반환하세요. 이후 별도 프로세스가 그 레코드를 가져와 작업을 수행합니다.
자주 나오는 용어 몇 가지:
- Job: "사용자 123에게 환영 이메일 전송" 같은 한 단위의 작업.
- Worker: 작업을 꺼내 실행하는 코드.
- Attempt: 작업을 한 번 시도하는 시도.
- Schedule: 작업을 언제 실행할지(지금 또는 나중).
- Queue: 워커가 가져갈 때까지 작업이 기다리는 곳.
가장 단순한 흐름은 다음과 같습니다:
- Enqueue: 앱이 작업 레코드(type, payload, 실행 시간)를 저장합니다.
- Claim: 워커가 다음 사용 가능한 작업을 찾아 "잠금"을 걸어 한 워커만 실행하게 합니다.
- Run: 워커가 작업(전송, 생성, 전달)을 수행합니다.
- Finish: 완료로 표시하거나 실패를 기록하고 다음 실행 시간을 설정합니다.
작업량이 적당하고 이미 데이터베이스를 가지고 있다면 데이터베이스 기반 큐가 충분한 경우가 많습니다. 이해하기 쉽고 디버그하기 쉬우며 이메일 작업 처리나 웹후크 전달 신뢰성 같은 일반적인 요구를 충족합니다.
스트리밍 플랫폼은 매우 높은 처리량, 많은 독립 소비자, 또는 여러 시스템에 걸친 큰 이벤트 이력을 재생할 필요가 있을 때 의미가 있습니다. 수십 개 서비스에서 시간당 수백만 이벤트를 처리한다면 Kafka 같은 도구가 도움이 됩니다. 그 전까지는 데이터베이스 테이블과 워커 루프가 많은 실제 큐 요구를 커버합니다.
모든 작업에 대해 최소한으로 추적해야 할 데이터
데이터베이스 큐는 각 작업 레코드가 세 가지 질문에 빠르게 답할 수 있을 때만 건전하게 유지됩니다: 무엇을 할지, 다음에 언제 시도할지, 지난번에 무슨 일이 있었는지. 이 부분을 잘하면 운영은 지루해집니다(목표가 바로 그겁니다).
페이로드에 저장할 것(그리고 저장하지 않을 것)
작업을 수행하는 데 필요한 가장 작은 입력만 저장하세요. 전체 렌더된 출력은 저장하지 마세요. 좋은 페이로드는 ID와 몇 가지 파라미터입니다, 예: { "user_id": 42, "template": "welcome" }.
큰 블롭(완성된 HTML 이메일, 큰 리포트 데이터, 거대한 웹후크 본문)을 저장하면 데이터베이스가 빨리 커지고 디버깅이 더 어려워집니다. 작업이 큰 문서를 필요로 하면 대신 참조를 저장하세요: report_id, export_id, 또는 파일 키. 워커가 실행될 때 전체 데이터를 가져오면 됩니다.
투자 대비 효과가 큰 필드들
최소한 다음을 마련하세요:
- job_type + payload:
job_type은 핸들러(send_email, generate_report, deliver_webhook)를 선택합니다.payload는 ID와 옵션 같은 작은 입력을 담습니다. - status: 명확하게 유지하세요(예:
queued,running,succeeded,failed,dead). - attempt tracking:
attempt_count와max_attempts로 더 이상 재시도하지 않을 시점을 정합니다. - 시간 필드:
created_at과next_run_at(언제 실행 자격이 생기는지).started_at과finished_at을 추가하면 느린 작업 가시성이 좋아집니다. - idempotency + last error: 중복 효과를 막는
idempotency_key와 실패 이유를 바로 볼 수 있게 하는last_error.
아이덴포텐시는 겉보기에 복잡해 보일 수 있지만 아이디어는 단순합니다: 같은 작업이 두 번 실행되면 두 번째 실행은 위험한 동작을 하지 않도록 해야 합니다. 예를 들어 웹후크 전달 작업은 webhook:order:123:event:paid 같은 아이덴포텐시 키를 사용해 재시도나 타임아웃이 겹쳐도 동일한 이벤트를 두 번 전달하지 않게 할 수 있습니다.
또한 몇 가지 기본 숫자를 초기에 캡처하세요. 큰 대시보드가 필요하지 않습니다. 단지 다음을 알려주는 쿼리면 됩니다: 큐에 몇 개의 작업이 있는지, 몇 개가 실패 중인지, 가장 오래된 큐 작업의 연령은 얼마인지.
단계별: 오늘 바로 만들 수 있는 간단한 데이터베이스 큐
이미 데이터베이스가 있다면 새로운 인프라를 추가하지 않고 백그라운드 큐를 시작할 수 있습니다. 작업은 행이고 워커는 만료된 행을 계속 가져와 처리하는 프로세스입니다.
1) jobs 테이블 생성
테이블은 작고 단순하게 유지하세요. 나중에 작업을 실행하고 재시도하며 디버그할 수 있을 정도의 필드는 필요합니다.
CREATE TABLE jobs (
id bigserial PRIMARY KEY,
job_type text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'queued', -- queued, running, done, failed
attempts int NOT NULL DEFAULT 0,
next_run_at timestamptz NOT NULL DEFAULT now(),
locked_at timestamptz,
locked_by text,
last_error text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX jobs_due_idx ON jobs (status, next_run_at);
Postgres(Go 백엔드에서 흔함)를 사용한다면 jsonb는 { "user_id":123,"template":"welcome" } 같은 작업 데이터를 저장하는 현실적인 방법입니다.
2) 안전하게 큐에 넣기(특히 사용자 액션의 경우)
사용자 액션이 작업을 트리거해야 한다면(이메일 전송, 웹후크 발송), 가능하면 메인 변경과 같은 데이터베이스 트랜잭션에서 작업 행을 작성하세요. 그러면 주요 쓰기 직후 크래시가 나서 "사용자 생성은 되었지만 작업이 없다"는 상황을 막을 수 있습니다.
예: 사용자가 가입할 때 사용자 행과 send_welcome_email 작업을 한 트랜잭션에서 삽입하세요.
3) 확장 가능한 워커 루프 실행
워커는 동일한 사이클을 반복합니다: 만료된 한 작업을 찾아 원자적으로 클레임하고 처리한 뒤 완료 표시하거나 재시도를 예약합니다.
실무에서는 다음과 같습니다:
status='queued'이고next_run_at <= now()인 작업을 한 건 선택- 원자적으로 클레임(예: Postgres에서
SELECT ... FOR UPDATE SKIP LOCKED) status='running',locked_at=now(),locked_by='worker-1'로 설정- 작업 처리
- 완료(
done/succeeded)로 표시하거나last_error를 기록하고 다음 시도를 예약
여러 워커가 동시에 실행될 수 있습니다. 클레임 단계가 중복 선택을 막아줍니다.
4) 종료 시 작업을 망가뜨리지 않기
종료할 때는 새 작업 수락을 중단하고 현재 작업을 마친 뒤 종료하세요. 프로세스가 작업 중 죽으면 간단한 규칙을 사용하세요: 일정 시간 이상 running 상태로 묶여 있는 작업은 주기적 "리퍼" 작업이 다시 큐에 넣도록 처리합니다.
Koder.ai로 개발한다면 이메일, 리포트, 웹후크 용도의 이 데이터베이스-큐 패턴은 특화된 큐 서비스를 추가하기 전의 안정적인 기본값입니다.
혼란을 일으키지 않는 재시도와 백오프
재시도는 현실 세계가 지저분할 때 큐가 차분하게 유지되는 방법입니다. 명확한 규칙이 없으면 재시도는 소음을 만들고 사용자에게 스팸을 보내며 API를 두들기고 실제 버그를 숨깁니다.
무엇을 재시도하고 무엇을 빠르게 실패 처리할지 결정하는 것부터 시작하세요.
일시적 문제를 재시도하세요: 네트워크 타임아웃, 502/503, 속도 제한, 또는 짧은 데이터베이스 연결 장애.
작업이 성공할 수 없는 경우에는 빠르게 실패 처리하세요: 이메일 주소 누락, 페이로드가 잘못되어 400을 반환한 웹후크, 삭제된 계정을 위한 리포트 요청 등.
백오프는 시도 사이의 대기입니다. 선형 백오프(5초, 10초, 15초)는 단순하지만 트래픽 파동을 만들 수 있습니다. 지수적 백오프(5초, 10초, 20초, 40초)는 부하를 더 잘 분산시키며 웹후크와 서드파티 공급자에 보통 더 안전합니다. 지터(작은 무작위 추가 지연)를 더해 수천 개 작업이 동시에 같은 초에 재시도하지 않도록 하세요.
운영에서 잘 동작하는 규칙들:
- 일시적 오류(타임아웃, 429, 5xx)에만 재시도
- 지수적 백오프와 지터 사용
- 시도 횟수를 제한하고 그 후에는 실패로 표시
- 워커가 걸려있지 않도록 시도별 타임아웃 설정
- 모든 작업을 아이덴포텐트하게 만들어 재시도가 중복을 만들지 않도록 함
최대 시도 횟수는 피해를 제한하는 장치입니다. 많은 팀에서는 5~8회의 시도가 충분합니다. 그 이후에는 재시도를 중단하고 검토를 위한 데드레터 플로우로 옮기세요.
타임아웃은 "좀비" 작업을 막습니다. 이메일은 시도당 1020초, 웹후크는 수신자가 다운일 가능성이 크므로 510초 정도의 짧은 제한을 두는 것이 일반적입니다. 리포트 생성은 몇 분을 허용할 수 있지만 여전히 강제 종료 시간은 두어야 합니다.
Koder.ai에서 이 기능을 구현한다면 should_retry, next_run_at, 그리고 아이덴포텐시 키를 1등 시민처럼 다루세요. 이런 작은 디테일이 문제가 생겼을 때 시스템을 조용하게 유지합니다.
데드레터 처리와 간단한 운영
데드레터 상태는 재시도가 더 이상 안전하거나 유용하지 않을 때 작업이 가는 곳입니다. 침묵하는 실패를 검색하고 조치할 수 있는 항목으로 바꿉니다.
데드레터 작업에 저장할 것
무슨 일이 있었는지 이해하고 재실행할 수 있을 만큼 충분히 저장하되, 비밀 정보는 주의하세요.
저장할 것:
- 작업 입력(paload) 원본, job 타입과 버전
- 마지막 오류 메시지와 짧은 스택 트레이스(스택이 없다면 오류 코드)
- 시도 횟수, 첫 실행 시간, 마지막 실행 시간, (예약되어 있었다면) 다음 실행 시간
- 워커 식별자(서비스 이름, 호스트)와 로그를 위한 상관 ID
- 데드레터 이유(타임아웃, 검증 오류, 공급자로부터의 4xx 등)
페이로드에 토큰이나 개인 데이터가 포함되어 있으면 저장 전에 마스킹하거나 암호화하세요.
간단한 트리아지 워크플로우
작업이 데드레터에 도달하면 빠르게 결정하세요: 재시도, 수정, 무시.
- 재시도: 외부 장애나 타임아웃의 경우.
- 수정: 잘못된 데이터(메일 누락, 잘못된 웹후크 URL)나 코드 버그의 경우.
- 무시: 드물지만 유효할 수 있음(예: 고객이 계정을 삭제해 더 이상 관련이 없을 때). 무시한다면 이유를 기록해 작업이 사라지지 않도록 하세요.
수동 재큐는 새로운 작업을 생성하고 옛 작업은 불변으로 유지하는 것이 가장 안전합니다. 데드레터 작업에 누가 재큐했는지, 언제, 왜 했는지를 기록하고 새 ID로 새 작업을 넣으세요.
알림용으로는 보통 진짜 문제를 의미하는 신호를 관찰하세요: 데드레터 건수가 빠르게 증가하거나 동일 오류가 여러 작업에서 반복되거나 오래된 큐 작업이 클레임되지 않는 경우 등.
Koder.ai를 사용하면 실패가 급증하는 나쁜 릴리스를 조사할 때 스냅샷과 롤백이 도움이 됩니다. 빠르게 되돌릴 수 있기 때문입니다.
마지막으로 공급자 다운을 대비한 안전장치를 더하세요. 공급자별 전송 속도 제한을 두고 회로 차단기(circuit breaker)를 사용하세요: 웹후크 엔드포인트가 심하게 실패하면 짧은 기간 동안 새 시도를 중단해 그들의 서버(그리고 여러분의 서버)를 과부하시키지 마세요.
이메일, 리포트, 웹후크에 대한 패턴
각 작업 타입에 대해 무엇을 성공으로 볼지, 무엇을 재시도할지, 절대 두 번 일어나면 안 되는 것은 무엇인지 명확한 규칙을 두면 큐가 가장 잘 동작합니다.
이메일. 대부분의 이메일 실패는 일시적입니다: 제공자 타임아웃, 속도 제한, 짧은 장애 등. 이런 경우 재시도하고 백오프를 적용하세요. 더 큰 위험은 중복 전송이므로 이메일 작업은 아이덴포텐트하게 만드세요. user_id + template + event_id 같은 안정적 중복 키를 저장하고 이미 전송된 경우 전송을 거부하세요.
템플릿 이름과 버전(또는 렌더된 제목/본문의 해시)을 저장해두면 작업을 다시 실행할 때 동일한 내용을 다시 보낼지, 최신 템플릿으로 재생성할지 선택할 수 있습니다. 제공자가 메시지 ID를 반환하면 지원 추적을 위해 저장하세요.
리포트. 리포트 실패 양상은 다릅니다. 몇 분 걸릴 수 있고 페이지네이션 한계에 걸리거나 한 번에 모두 처리하면 메모리가 부족해질 수 있습니다. 작업을 작은 조각으로 분할하세요. 일반적인 패턴은 하나의 "리포트 요청" 작업이 여러 개의 "페이지"(또는 "청크") 작업을 생성해 각 데이터 조각을 처리하는 것입니다.
사용자를 기다리게 하지 말고 나중에 다운로드할 결과를 저장하세요. 이는 report_run_id로 키된 데이터베이스 테이블이 될 수도 있고 파일 참조와 메타데이터(상태, 행 수, 생성 시간)가 될 수도 있습니다. UI가 "처리 중"과 "준비됨"을 표시할 수 있도록 진행 필드를 추가하세요.
웹후크. 웹후크는 속도보다 전달 신뢰성이 중요합니다. 모든 요청에 서명(HMAC 등)하고 재재생을 막기 위해 타임스탬프를 포함하세요. 수신자가 나중에 성공할 가능성이 있을 때만 재시도하세요.
간단한 규칙 집합:
- 타임아웃과 5xx 응답에 대해 백오프 및 최대 시도 횟수로 재시도
- 대부분의 4xx 응답은 영구 실패로 간주하고 재시도 중단
- 디버깅을 위해 마지막 상태 코드와 짧은 응답 본문을 기록
- 수신자가 중복을 안전하게 무시할 수 있도록 아이덴포텐시 키 사용
- 페이로드 크기 제한 및 실제 보낸 내용 로그 기록
정렬(ordering) 및 우선순위. 대부분의 작업은 엄격한 정렬이 필요하지 않습니다. 순서가 중요할 때는 보통 키별(사용자별, 송장별, 웹후크 엔드포인트별)로 중요합니다. group_key를 추가하고 키당 한 건의 인플라이트 작업만 실행하도록 하세요.
우선순위가 필요하면 긴급 작업과 느린 작업을 분리하세요. 큰 리포트 대기열이 비밀번호 재설정 이메일을 지연시키면 안 됩니다.
예: 구매 후 (1) 주문 확인 이메일, (2) 파트너 웹후크, (3) 리포트 업데이트 작업을 큐에 넣습니다. 이메일은 빠르게 재시도하고, 웹후크는 더 긴 백오프와 재시도 정책을 적용하며, 리포트는 낮은 우선순위로 나중에 실행합니다.
현실적인 예: 가입 흐름 + 웹후크 + 야간 리포트
사용자가 앱에 가입합니다. 세 가지 일이 발생해야 하지만 어느 것도 가입 페이지를 느리게 해서는 안 됩니다: 환영 이메일 전송, CRM에 웹후크로 알림 전송, 사용자를 야간 활동 리포트에 포함시키기.
가입 시 큐에 들어가는 것
사용자 레코드를 만든 직후 데이터베이스 큐에 세 개의 작업 행을 작성하세요. 각 행은 타입, 페이로드(예: user_id), 상태, 시도 횟수, next_run_at 타임스탬프를 가집니다.
전형적인 라이프사이클은 다음과 같습니다:
queued: 생성되어 워커를 기다리는 상태running: 워커가 클레임한 상태succeeded: 완료되어 더 이상 작업 없음failed: 실패해 나중에 재시도되거나 재시도 한도가 있음dead: 너무 여러 번 실패해 사람이 봐야 하는 상태
환영 이메일 작업에는 welcome_email:user:123 같은 아이덴포텐시 키가 포함됩니다. 전송 전에 워커는 완료된 아이덴포텐시 키 테이블을 확인하거나 고유 제약을 통해 중복을 막습니다. 작업이 두 번 실행되면 두 번째 실행은 키를 보고 전송을 건너뜁니다. 이로써 중복 환영 이메일을 막을 수 있습니다.
실패와 복구 사례
CRM 웹후크 엔드포인트가 다운되었습니다. 웹후크 작업이 타임아웃으로 실패합니다. 워커는 백오프(예: 1분, 5분, 30분, 2시간)와 소량의 지터를 더한 재시도를 예약합니다.
최대 시도를 초과하면 작업은 dead가 됩니다. 사용자는 여전히 가입되었고 환영 이메일을 받았고 야간 리포트 작업은 정상적으로 실행될 수 있습니다. 문제가 있는 것은 CRM 알림 하나뿐이며 이는 가시화됩니다.
다음 날 아침 지원 담당자가 오래된 로그를 뒤지지 않고 처리할 수 있습니다:
- 데드 작업을 타입(
webhook.crm등)으로 필터링 - 마지막 오류 메시지를 읽고 페이로드가 올바른지 확인
- CRM이 다시 올라왔는지 확인
- 작업을 재큐(데드 -> queued, 시도 횟수 재설정)하거나 대상지를 일시 중단
Koder.ai 기반 앱이라면 같은 패턴을 적용하세요: 사용자 흐름은 빠르게 유지하고 부수 효과는 작업으로 밀어 넣으며 실패를 검사하고 다시 실행하기 쉽게 만드세요.
큐를 신뢰할 수 없게 만드는 흔한 실수들
큐를 망치는 가장 빠른 방법은 큐를 선택사항으로 여기는 것입니다. 팀들은 종종 "이번 한 번은 요청에서 이메일을 보내자"며 시작합니다. 그러다 비밀번호 재설정, 영수증, 웹후크, 리포트 내보내기가 번지고 앱이 느려지며 타임아웃이 늘어나고 서드파티의 작은 문제 하나가 여러분의 장애가 됩니다.
또 다른 흔한 함정은 아이덴포텐시를 생략하는 것입니다. 작업이 두 번 실행될 수 있다면 두 번째 실행이 두 개의 결과를 만들면 안 됩니다. 아이덴포텐시가 없으면 재시도는 중복 이메일, 반복된 웹후크 이벤트, 더 나쁜 결과를 만듭니다.
세 번째 문제는 가시성 부족입니다. 오직 지원 티켓으로만 실패를 알게 된다면 큐는 이미 사용자를 해치고 있는 상태입니다. 상태별 작업 수와 검색 가능한 last_error를 보여주는 기본 내부 뷰만 있어도 많은 시간을 절약할 수 있습니다.
초기에 나타나는 신뢰성 킬러들
간단한 큐에서도 빨리 보이는 몇 가지 문제:
- 실패 시 즉시 재시도하는 것. 공급자가 다운이면 빠른 재시도는 자체 트래픽 스파이크를 만듭니다.
- 느린 작업과 긴급 작업을 섞는 것. 10분짜리 리포트가 "이메일 인증"을 막아선 안 됩니다.
- 오류를 영원히 일시적이라고 취급하는 것. 절대 성공하지 않을 작업이 계속 순환하면 실제 문제를 가립니다.
- 페이로드 버전의 소유자가 없음. 작업 형태를 바꾸면 오래된 작업이 실패할 수 있습니다.
- 속도 제한 무시. 큐는 여러분을 차단하는 공급자를 과부하시킬 수 있습니다.
백오프는 자체적으로 만든 장애를 막습니다. 1분, 5분, 30분, 2시간 같은 단순한 스케줄도 실패를 더 안전하게 만듭니다. 또한 최대 시도 제한을 설정해 깨진 작업이 멈추고 가시화되도록 하세요.
Koder.ai에서 개발한다면 이런 기본을 기능과 함께 바로 배포하는 것이 도움이 됩니다. 나중에 정리하는 것이 아니라 처음부터 적용하세요.
빠른 체크리스트와 다음 단계
더 많은 도구를 추가하기 전에 기본이 튼튼한지 확인하세요. 각 작업을 안전하게 클레임하고 재시도하고 검사할 수 있다면 데이터베이스 기반 큐는 잘 작동합니다.
빠른 신뢰성 체크리스트:
- 모든 작업에: id, type, payload, status, attempts, max_attempts, run_at/next_run_at, last_error가 있음
- 워커가 작업을 안전하게 클레임하고(한 워커가 하나의 작업) 크래시 후 복구(락 타임아웃 + 리퍼) 가능
- 각 작업에 명확한 타임아웃이 있어 걸린 작업이 재시도 가능하도록 함
- 재시도는 한도 있고 재시도 지연이 커짐(백오프)으로써 떼 지연(thundering herd)을 피함
- 데드레터 상태(또는 테이블)와 작업을 다시 실행하거나 폐기할 수 있는 명확한 방법 존재
다음으로 처음 세 가지 작업 타입을 정하고 규칙을 적어보세요. 예: 비밀번호 재설정 이메일(빠른 재시도, 짧은 최대 시도), 야간 리포트(낮은 재시도, 더 긴 타임아웃), 웹후크 전달(더 많은 재시도, 긴 백오프, 영구적 4xx일 때 중단).
데이터베이스 큐가 충분하지 않을 때를 판단하기 어렵다면 다음 신호를 보세요: 많은 워커의 행 수준 컨텐션, 여러 작업 타입에 걸친 엄격한 순서 요구, 하나의 이벤트가 수천 개 작업으로 팬아웃되는 경우, 또는 다른 팀이 소유한 여러 서비스가 소비하는 교차 서비스 소비.
빠른 프로토타입을 원하면 Koder.ai의 플래닝 모드에서 흐름을 스케치하고, jobs 테이블과 워커 루프를 생성하고 스냅샷과 롤백으로 배포 전 반복하세요. (koder.ai)
자주 묻는 질문
언제 작업을 요청 내에서 처리하지 않고 백그라운드 작업으로 옮겨야 하나요?
작업이 1~2초 이상 걸리거나 네트워크 호출(이메일 제공자, 웹후크 엔드포인트, 느린 쿼리)에 의존한다면 백그라운드 작업으로 옮기세요.
사용자 요청은 입력 검증, 주요 데이터 변경 기록, 작업 큐잉, 빠른 응답 반환에 집중하게 하세요.
데이터베이스 기반 큐로 충분한가요, 아니면 메시지 브로커가 필요한가요?
다음과 같은 경우에는 데이터베이스 기반 큐로 시작하세요:
- 이미 PostgreSQL을 사용 중이고 처리량이 적당할 때
- 디버그하기 쉽고 가장 단순한 방법을 원할 때
- 한 서비스가 큐잉과 처리를 모두 소유할 때
많은 처리량, 다수의 독립 소비자, 또는 여러 시스템에 걸친 이벤트 재생이 필요해지면 브로커/스트리밍 도구를 추가하세요.
모든 작업 레코드에 어떤 필드를 넣어야 하나요?
다음 질문에 답할 최소한의 필드를 추적하세요: 무엇을 할지, 언제 다음 시도인지, 지난번에 무슨 일이 있었는지.
실용적 최소 구성:
job_type,payloadstatus(queued,running,succeeded,failed,dead)attempts,max_attemptsnext_run_at, 그리고created_atlocked_at,locked_bylast_erroridempotency_key(또는 다른 중복 방지 메커니즘)
작업 페이로드에는 무엇을 넣고 무엇을 피해야 하나요?
작업에는 입력만 저장하고 큰 출력은 저장하지 마세요.
좋은 페이로드 예:
- ID와 작은 옵션들(예:
user_id,template,report_id)
피해야 할 것:
- 렌더된 HTML 이메일 전체
- 큰 리포트 데이터 블롭
- 거대한 웹후크 본문
작업에 큰 데이터가 필요하면 report_run_id나 파일 키 같은 참조를 저장하고, 워커가 실행될 때 실제 콘텐츠를 가져오세요.
여러 워커가 동일한 작업을 선택하지 않게 하려면 어떻게 하나요?
핵심은 원자적(claim)한 클레임 단계로 두 워커가 같은 작업을 가져가지 못하게 하는 것입니다.
Postgres에서 흔한 접근법:
- 잠금과 함께 적격 행을 선택(
FOR UPDATE SKIP LOCKED) - 즉시 상태를
running으로 바꾸고locked_at/locked_by를 설정
이렇게 하면 워커를 수평으로 확장해도 동일한 행을 중복 처리하지 않습니다.
중복 이메일이나 중복 웹후크 전달을 어떻게 방지하나요?
작업은 언제든 두 번 실행될 수 있다고 가정하고 부작용을 안전하게 만드세요.
간단한 패턴:
welcome_email:user:123같은idempotency_key를 추가- 고유 제약이나 완료된 키 테이블로 중복을 방지
- 반복 실행 시 키를 검사하고 전송/전달을 건너뜀
이 방법은 이메일과 웹후크의 중복 전송을 막는 데 특히 중요합니다.
어떤 재시도와 백오프 전략으로 시작해야 하나요?
명확하고 지루한 기본 정책을 사용하세요:
- 일시적인 실패(타임아웃, 429, 5xx)에만 재시도
- 지수적 백오프와 지터(작은 무작위 지연)
- 시도 횟수 제한(보통 5~8회)
- 각 시도에 대한 타임아웃 설정
영구 오류(예: 없는 이메일 주소, 잘못된 페이로드, 대부분의 4xx 웹후크 응답)는 바로 실패 처리하세요.
데드레터 작업이란 무엇이고 언제 사용해야 하나요?
데드레터는 “더 이상 재시도하지 말고 가시화하라”는 의미입니다. 다음 경우 데드레터를 사용하세요:
max_attempts를 초과했을 때- 오류가 분명히 영구적일 때
- 재시도하면 피해가 발생할 때(스팸, 반복된 잘못된 웹후크 등)
행동을 취할 수 있도록 다음을 저장하세요:
- job 타입, 페이로드(필요 시 마스킹)
- 시도 횟수와 타임스탬프
last_error와 웹후크의 마지막 상태 코드
재실행 시에는 기존 데드레터 항목을 불변으로 두고 새 작업을 생성하는 것이 안전합니다.
크래시 후 실행 상태에 멈춘 작업을 어떻게 처리하나요?
“실행 중(running)”으로 남은 작업은 다음 두 규칙으로 처리하세요:
- 모든 시도에 타임아웃을 두어 작업이 영원히 실행되지 않도록 함
- 주기적 리퍼(reaper)가 오래된
running작업을 찾아 재큐하거나 실패로 표시
이렇게 하면 워커 크래시에서 시스템이 수동 정리 없이 복구할 수 있습니다.
우선순위와 순서를 어떻게 처리하나요(리포트가 중요한 이메일을 지연시키지 않게)?
느린 작업이 중요한 작업을 막지 않도록 분리하세요:
- 긴급 작업(비밀번호 재설정, 이메일 인증)은 고우선순위 큐에 두기
- 대용량 리포트 같은 무거운 작업은 저우선순위 큐에 두기
순서(ordering)가 중요하면 보통 키별(사용자별, 웹후크 엔드포인트별)로 중요합니다. group_key를 추가하고 키당 하나의 인플라이트 작업만 허용해 지역적 순서를 보장하세요.