이 글 공유하기

🎧 ▶ 5분 브리핑으로 듣기
▶ 오디오북 재생 (Google Drive)
NotebookLM 오디오 개요 (AI 생성)
📕 전자책 전문 읽기 큐의 기술: 배경 작업 · 33쪽
브라우저가 PDF를 바로 표시하지 못합니다. 새 탭에서 열기
이 글은 요약해 다시 쓴 것이고, PDF가 전문입니다.

이 글은 사용자 요청 안에서 이메일, 이미지 렌더링, LLM 호출까지 직접 처리하는 1인 개발자와 소규모 SaaS 팀을 위한 것입니다. 읽고 나면 얻는 것이 하나 있습니다. 배경 작업의 진짜 위험은 조용한 소실입니다. 그리고 사라진 작업을 잡아내는 큐 운영의 규율.

결론부터 말합니다. 큐는 빠르게 만드는 도구가 아닙니다. 큐는 지금 당장 하지 않아도 되는 일을 요청 밖으로 내보내는 장치이고, 그 순간 시스템은 사용자에게 새로운 계약을 맺게 됩니다. 언제 끝나는습니까, 실패하면 어떻게 되는습니까. 배경 작업 시스템의 성패는 작업이 얼마나 빨리 도는지로 재지 않습니다. 사라진 작업을 얼마나 신뢰할 수 있게 잡아내느냐로 재습니다.

반복되는 장면이 있습니다. 보고서 버튼을 누르면, 하나의 요청 안에서 이메일을 보내고 이미지를 렌더링하고 LLM을 호출합니다. 사용자가 30초를 기다렸다가 브라우저가 포기하면, 사용자는 다시 누릅니다. 이메일 두 통, 이미지 두 장, LLM 두 번. 로그를 보면 모든 단계가 성공을 반환했습니다. 아무것도 깨지지 않았다는 기록뿐입니다. 배경 작업 문제가 시작되는 모습은 대개 이런 형태이고, 그 원인은 거의 언제나 경계가 잘못된 데 있습니다. 이 글은 그 경계부터 시작해, 작업의 모양, 실패의 사각지대, 관측까지 순서대로 논증합니다.

배경 작업은 실패하지 않고 사라진다 개념을 형상화한 이미지 글의 핵심 개념을 형상화했습니다.

요청 안에 뭘 두고 밖으로 뭘 내보내는가

동기 경로는 단순합니다. 요청이 들어와, 처리되고 결과가 돌아갑니다. 그 사이 사용자의 화면은 막혀 있습니다. 조회, 검색, 결제 승인은 이 경로에 잘 맞는 일입니다. 사용자가 그 자리에서 결과를 기다리고 처리가 빠르고 결정을 내리기 쉽습니다.

비동기 경로는 “받았다”는 사실만 즉시 돌려줍니다. 일은 나중에 일어나고 결과는 이메일이나 상태 변경, 또는 푸시로 도착합니다. 사용자의 대기 시간은 거의 0이 됩니다. 대신 계약이 생깁니다. 큐가 지켜야 할 계약은 둘입니다. 언제 끝나는습니까, 실패하면 어떻게 되는습니까.

모던하냐는 질문은 여기서 물러납니다. 남는 질문은 둘입니다. 사용자가 얼마나 기다려줄 수 있는가, 그리고 서비스가 일을 늦게 해도 되는습니까. 사용자가 화면을 보며 기다리고 있다면 동기입니다. 2초가 걸려도 그렇습니다. 비동기는 사용자가 자리를 뜬 뒤 알아서 처리해도 되는 일에만 쓰입니다.

새로운 일이 생겼을 때, 경로에 넣기 전에 몇 가지를 묻습니다. 사용자가 지금 결과를 봐야 합니까. 얼마나 걸립니까. 대략적인 가이드라인은 이렇습니다. 100ms 이하는 동기여도 문제없고 1초를 넘으면 큐 후보다 [추정]. 이 라인 자체보다 중요한 것은, 처리 시간이 내 손에서 벗어난다는 사실입니다. DB 락, 외부 API, 불안정한 네트워크. 예상보다 실행을 늘릴 수 있는 요인이 보이면, 그 일부터 요청 경로에서 빼는 것이 안전합니다.

경계를 흐리게 하는 유혹은 속도입니다. 보고서가 빠르면 동기에서 처리해도 괜찮을 것이라는 계산이 먼저 나옵니다. 그러나 기준은 사용자의 대기 자세입니다. 화면 앞에서 결과를 보는 일이라면 2초여도 동기입니다. 사용자가 자리를 뜬 뒤 도착해도 되는 일이라면 30초여도 큐입니다. 빠르다는 사실 하나가 경계를 움직이지 않습니다. 경계는 도구 고르기 전에, 어떤 계약으로 사용자를 대할지를 정한 후에야 섭니다.

핵심 개념 요약 인포그래픽 1 NotebookLM이 소스를 종합해 생성한 인포그래픽입니다.

작업은 다시 돌 수 있는 최소 단위로

경계가 정해지면 다음 질문은 모양입니다. 큐는 결국 목록인데, 배경 작업 파이프라인의 품질을 두 가지가 정합니다. 들어가는 작업의 모양, 그리고 그 작업을 꺼내서 돌리는 워커의 루프. 이 섹션에서 다룰 것은 전자입니다.

작업의 크기가 실패의 파급 범위를 정합니다. 작업은 안전하게 다시 실행할 수 있는 한 단위의 조작이어야 합니다. 세 가지 조건이 있습니다. 입력이 명확하고 조작이 하나이며 완료를 검증할 수 있어야 합니다.

흔한 반례는 묶어 넣기입니다. 주문 처리라는 작업에 결제 확인, 발송, 이메일, 적립을 모두 담은 경우. 마지막에 이메일이 실패하면 재시도 시 전체가 다시 돌고, 발송은 두 번 일어납니다. 두 번째 박스가 창고에서 나가는 순간, 시스템은 정확히 설계대로 동작한 결과입니다. 사이에 의존성이 있으면, 조정은 별도의 부모 작업이나 상태 기계가 맡습니다. 자식들은 각자 재시도 가능하게 만듭니다.

좋은 작업은 목적어가 하나인 동사입니다. 이메일 1통 전송은 좋은 작업이고 고객에게 연락하기는 나쁜 작업입니다. 무엇을, 어떤 식으로, 어떻게 완료가 되는지 알 수 없기 때문입니다. 아래 표는 같은 파이프라인 안에서 두 모양이 공존하는 모습을 비교합니다.

좋은 작업 나쁜 작업
이메일 1통 전송 주문 전체 처리
이미지 리사이즈 1개 결제 + 발송 + 적립
LLM 호출 1회 보고서 전체 생성
해지 절차 1단계 고객에게 연락하기

페이로드는 참조를 실어 나릅니다. 작업에 사용자 기록의 복사본을 담아 두면 안 됩니다. user_id만 넣고 실행할 때 DB에서 최신으로 가져옵니다. 큐 속에 고인 복사본과 DB의 상태가 다른 것은, 찾기 힘든 버그의 원천입니다. 사용자의 주소가 바뀌었어도, 큐에 고여 있는 옛 주소로 메일이 갑니다. 큐는 무엇을 할 일을 전하는 곳이고 지금은 어떤 상태라는 사실은 DB가 담습니다.

엔큐는 가볍게 만듭니다. 엔큐는 작업을 큐에 쓰는 행위입니다. 엔큐 시점에 파일을 가져오거나 API를 부르는 일이 필요하다면, 그 일 자체를 별도 작업으로 만듭니다. 엔큐는 빠른 기록이어야 합니다. 무거워진 엔큐는 동기 경로에 또다시 긴 블록을 만들어, 이 섹션의 전제인 경계 설정을 무너뜨립니다.

the-queue-discipline 슬라이드 1

재실행은 막을 수 없고 무해하게만 만들 수 있다

워커는 죽습니다. 배포, OOM, 패닉. 워커가 작업을 실행하다가 사라지면, 그 작업은 ack되지 못합니다. 가시성 타임아웃이 지나면 작업은 다시 돌아오고 다른 워커가 다시 실행합니다. 이 회복은 필수입니다. 워커가 죽어도 일이 돌아오게 해 주는 것이 큐의 존재 이유이기 때문입니다. 대가는 중복 실행입니다. 사이드 이펙트가 두 번 일어날 수 있습니다.

그래서 최소 한 번과 멱등성이 파이프라인의 핵심이 됩니다. 재실행을 막을 수 있는 방법은 없습니다. 만들어지는 순간, 멱등성이 없는 작업은 재실행의 위험을 품은 상태입니다. 멱등성이란 같은 조작이 여러 번 돌아가도, 결과가 한 번 실행한 것과 같은 성질입니다.

자연스럽게 멱등인 조작이 있고 그렇지 않은 조작이 있습니다. 주문 상태를 취소로 바꾸는 일은 몇 번을 돌려도 같은 상태가 생깁니다. 포인트를 올리는 일, 이메일을 보내는 일, 카드를 청구하는 일은 그렇지 않습니다. 매번 실행할 때마다 새로운 결과가 쌓입니다. 멱등이 아닌 단계 하나하나가, 재시도가 사용자에게 보일 실수가 되는 자리입니다.

기술은 몇 가지가 있습니다. 멱등 키입니다. 사용자가 버튼을 두 번 눌러도 같은 키로 들어오는 작업은 하나로 병합됩니다. 액션 전에 검사입니다. 보고서 행이 이미 존재하면 확인으로 끝냅니다. DB의 유니크 제약입니다. 중복 키 오류는 이미 완료했다는 증거로 읽습니다. 앞의 더블클릭 사례는 멱등 키 하나로 두 번째 이메일을 멈출 수 있습니다.

그러나 멱등성을 재시도해서 성공할 때까지 돌리는 정책과 섞으면 안 됩니다. 그건 별개의 병입니다. 영원히 재시도되는 작업은 워커를 차지하고 진짜 실패를 끊임없는 시도 사이에 숨깁니다. 재시도 횟수에 상한을 두고 상한에 닿으면 작업은 데드레터로 이동합니다. 데드레터는 실패한 작업이 누군가 볼 때까지, 세어지며 기다리는 곳입니다.

the-queue-discipline 슬라이드 2

워커의 죽음과, 멈춰 버린 작업

배경 파이프라인은 조용합니다. 아무 일이 없으면 존재를 느끼지 못합니다. 그래서 진짜 실력은 무언가 죽는 순간에 드러납니다. 일은 네 가지 사각지대에서 사라집니다. 워커가 죽는 것, 작업이 실패하지 않고 멈추는 것, 작업이 영원히 재시도되는 것, 그리고 ack과 사이드 이펙트가 어긋나는 것. 사각지대마다 붙잡는 자리가 다릅니다.

워커 사망은 가시성 타임아웃이 잡습니다. 다만 가시성 타임아웃은 범위입니다. 작업의 최대 실행 시간보다 길어야 합니다. 타임아웃이 30초이고 작업이 40초가 걸리면, 멀쩡한 워커의 작업이 다른 워커한테 탈취당해 이유 없이 두 번 돕니다. 동시에 잃을 수 있는 시간보다 짧아야 합니다. 타임아웃이 1시간이면, 죽은 작업을 1시간 동안 찾지 못합니다. 합리적인 시작점은 p95 실행 시간의 2에서 3배입니다 [추정]. 관측하면서 점차 조입니다.

사망에는 또 다른 종류가 있습니다. 메모리입니다. 큰 이미지를 풀거나 LLM 응답을 통째로 메모리에 올리는 작업에서 워커가 한계에 닿으면, 프로세스는 에러를 남기지 않고 끝납니다. 작업은 ack되지 않은 채 사라졌으므로, 가시성 타임아웃이 다시 붙잡아옵니다. 잡는 사각지대는 같고 원인이 다를 뿐입니다.

1인 팀에는 한 줄을 더 보탭니다. 워커를 다시 시작하는 것은 누가 합니까. 워커 하나가 죽으면, 그 워커를 살리는 일도 누군가의 몫입니다. systemd 같은 프로세스 매니저가 죽은 워커를 다시 띄워 주도록 해 두는 것은, 큐 운영의 기본 장비입니다. 매니저가 없다면, 일정 간격으로 워커가 살아있는가를 확인하는 워치독 크론 하나입니다. 둘 다 없으면, 워커 사망은 사람 눈으로 알아차리는 것뿐입니다. 그리고 사람들은 새벽에 깨어 있지 않습니다.

가장 까다로운 사각지대는 실패하지 않고 멈추는 작업입니다. 외부 API 호출에 걸린 작업은 에러를 던지지 않고, 단순히 반환하지 않습니다. 재시도 로직은 발동조차 못 합니다. 멈췄다는 사실을 로그가 기록하는 것도 없습니다. 이 사각지대를 잡는 것은 가시성 타임아웃뿐입니다. 앞 섹션에서 값을 정한 것이, 사실은 정류 감지 장치이기도 했던 셈입니다.

the-queue-discipline 슬라이드 3

ack과 사이드 이펙트, 데드레터까지

네 번째 사각지대는 ack과 사이드 이펙트의 어긋남입니다. ack은 사이드 이펙트가 끝난 후에만 씁니다. ack을 먼저 쓰면 일은 끝난 것으로 처리되어, 다시 돌아오지 않습니다. 그런데 사이드 이펙트가 여러 외부 호출로 이루어져 있으면, 호출 사이의 크래시는 일부를 남긴 채 멈추게 됩니다.

이럴 때 순서가 살림을 합니다. 되돌리기 힘든 호출은 마지막에 두고 앞의 호출들은 멱등으로 만듭니다. 이메일을 보내고 상태를 기록한 뒤 마지막에 카드를 청구합니다. 크래시가 어디서 터져도, 재실행이 안전해지는 순서입니다.

영원히 재시도되는 작업은 앞 섹션의 상한이 끊습니다. 재시도 횟수에 상한이 없으면, 실패한 작업이 계속 돌아다니며 워커를 차지합니다. 큐는 정지한 것이 아니라 돌아가는 것처럼 보이며, 진짜 실패는 그 움직임 뒤에 숨습니다. 상한을 두면, 실패한 작업은 데드레터에서 세어지며 기다립니다.

데드레터는 아침에 읽는 곳입니다. 같은 오류로 한꺼번에 쌓인 데드레터가 있다면, 그것은 N건의 실패가 아니라 한 건의 사고입니다. 외부 API가 내려간 날이면, 계속 실패하던 작업들이 상한에 닿아 데드레터로 몰려 옵니다. 읽는 순서도 같습니다. 먼저 외부 쪽을 확인하고 그 다음 내 코드입니다. 데드레터 수가 1을 넘은 순간, 사용자의 일이 일어나지 않았다는 증거가 생긴 것입니다.

붙잡는 자리를 정리하면, 이렇습니다. 워커의 죽음은 가시성 타임아웃과 워커 부활이 잡습니다. 정류는 가시성 타임아웃이 잡습니다. 무한 재시도는 상한과 데드레터가 잡습니다. ack과 사이드 이펙트의 어긋남은 순서와 멱등성이 잡습니다. 사각지대마다 붙잡는 자리가 다르지만 모든 죽음이 어딘가에 기록되도록 만드는 일은 같습니다.

the-queue-discipline 슬라이드 4

1인 팀이 볼 지표는 네 개면 충분하다

1인 팀에는 24시간 온콜이 없습니다. 새벽 3시에 큐가 막히면, 일어날지 말지를 그때서야 결정하게 됩니다. 컨텍스트 없이, 어둠 속에서. 그래서 관측의 설계 목표는 명확합니다. 조치할 일이 있을 때만 깨우는 것, 그리고 낮에 5분이면 건강을 확인할 수 있는 것.

지표는 많이 필요 없습니다. 네 개면 충분하고 각자의 역할이 다릅니다. 첫째는 최고령 작업의 대기 시간입니다. 정체의 진짜 계기입니다. 길이가 500개여도 1분 안에 비워진다면 괜찮고, 5개여도 3시간을 기다린 작업이 있다면 사고입니다. 몇 개가 쌓였는가보다, 쌓인 것 중 가장 오래된 것이 몇 시간을 기다렸는가를 봅니다.

둘째는 처리 시간의 p95입니다. 열화의 징조입니다. DB가 느려지고 외부 서비스가 느려지고 워커가 늙으면 여기부터 커집니다. 작업 유형별로 보는 것이 적절합니다. 이메일의 p95와 LLM의 p95를 합쳐서 보면 아무 의미가 없습니다. 각 유형은 각자의 모양을 가지고 있습니다. 같은 지표라도, 다른 스케일에서 읽어야 합니다.

셋째는 재시도율입니다. 외부 불안정의 징조입니다. LLM API나 SMTP가 그날 컨디션이 나쁘면, 여기가 가장 먼저 반응합니다. 재시도율이 갑작스레 오르면 대개 외부 쪽이 아픈 신호입니다. 내 코드보다는 그쪽을 먼저 확인하게 됩니다. 조사 순서를 정해 주는 지표입니다.

넷째는 데드레터 수입니다. 조용한 실패입니다. 1이 의미 있는 유일한 지표입니다. 사용자가 요청한 일이 일어나지 않았다는 증거가 하나 생겼기 때문입니다. 0이 정상이고 1이 넘는 순간 사용자에게 미안한 일이 생긴 증거입니다. 나머지 세 개는 추세이고 이것만은 사건입니다. 경계값은 시작점입니다. 최고령 대기 1시간 초과, 처리 p95가 통상의 2배, 재시도율 5퍼센트 초과, 데드레터 1건 이상 [추정]. 제품 리듬에 맞게 조입니다. 밤에만 도는 LLM 배치와 분 단위 이메일의 통상은, 같은 지표라도 다릅니다. 경계값은 법이 아니라, 새벽에 일어나도 5분이면 상황을 읽을 수 있게 해 주는 출발선입니다.

큐는 나중에를 안전하게 만드는 장치입니다. 일은 요청 밖으로 나가고 시스템은 끝낼 것과, 실패하면 말할 것과, 조용히 사라지지 않을 것을 약속합니다. 경계, 작은 멱등 작업, 범위로 정한 가시성 타임아웃, 데드레터, 네 개의 지표. 이것이 한꺼번에 처리하지 않는 규율의 전체입니다. 더 깊게 파고들고 싶은 분은, 이 글과 함께 준비된 전자책 큐의 기술: 배경 작업을 보시면 됩니다. 경계의 질문부터 데드레터 운영까지, 이 글이 간략히 한 지점들을 예시와 함께 펼쳐 쓴 책입니다.

참고 자료

본문의 최소 한 번 전달과 멱등성, 가시성 타임아웃의 범위, 워커 부활, 재시도 상한과 데드레터는 아래 자료와 대조해 보실 수 있습니다. 본문에 [추정]으로 표시한 경계값과 지수 배수는 저자의 운영 시작점입니다.

핵심 개념 요약 인포그래픽 2 NotebookLM이 소스를 종합해 생성한 인포그래픽입니다.

이 글 공유하기

태그: background-jobs, dead-letter, idempotency, job-queues, 관찰성, solo-developer, visibility-timeout

카테고리:

업데이트: