← 블로그 목록
백그라운드 작업큐크론서버리스재시도

큐 vs 크론 vs 즉시 실행: 오래 걸리는 작업을 어디로 보낼까

오래 걸리는 작업을 요청 안에서 처리할지, 큐에 넣을지, 크론으로 돌릴지 판단하는 기준과 각 방식에서 반드시 챙겨야 할 재시도·중복·감시를 정리했습니다.

Vibeollio 팀-

요청 안에서 다 처리하면 안 되는 일들

이미지 변환, 메일 발송, LLM 호출, 리포트 생성처럼 몇 초 이상 걸리는 작업을 HTTP 요청 안에서 그대로 처리하면 문제가 겹쳐서 옵니다. 응답이 늦어지고, 게이트웨이 타임아웃에 걸리고, 사용자가 답답해서 버튼을 다시 누르면 같은 작업이 두 번 돕니다.

해결책은 세 가지입니다. 요청 안에서 그냥 처리하기, 큐에 넣고 워커가 처리하기, 정해진 시각에 크론으로 처리하기. 셋은 대체재가 아니라 각각 다른 상황을 위한 것이라, 구분 기준부터 잡는 게 먼저입니다.

구분하는 질문 두 개

무엇이 작업을 시작시키는가? 사용자의 행동이면 즉시 실행이나 큐, 시각이면 크론입니다. 이게 1차 분기입니다.

결과를 지금 화면에 보여줘야 하는가? 보여줘야 하고 충분히 빠르면 즉시 실행, 오래 걸리면 큐에 넣고 진행 상태를 따로 알려줍니다.

이 두 질문이면 대부분 정해집니다. 나머지는 실패했을 때 재시도가 필요한지, 동시에 몇 개까지 돌려도 되는지 같은 운영 조건입니다.

1. 즉시 실행: 짧고, 실패해도 다시 누르면 되는 것

요청 처리 중에 그대로 끝내는 방식입니다. 구조가 가장 단순하니 조건만 맞으면 이게 정답입니다.

조건은 두 가지입니다. 수백 밀리초 안에 끝나고, 실패했을 때 사용자가 다시 시도하면 되는 일이어야 합니다. 폼 저장, 검색, 단순 계산이 여기 해당합니다.

여기서 흔한 함정이 하나 있습니다. 오래 걸리는 작업을 await 없이 호출하고 응답을 먼저 반환하는 방식입니다.

// 위험: 응답을 보낸 뒤 이 작업이 끝난다는 보장이 없다
sendWelcomeEmail(user);   // await 없음
return Response.json({ ok: true });

서버리스 환경에서는 응답을 반환하는 순간 실행 컨텍스트가 정지되거나 회수될 수 있어, 메일이 발송되지 않아도 아무도 모릅니다. 상시 서버라도 그 사이 프로세스가 재시작되면 그대로 사라집니다. 로그도 남지 않고 재시도도 없습니다.

즉 "응답만 먼저 주면 되겠지"는 큐의 대체재가 아닙니다. 유실을 감수해도 되는 작업(예: 통계 집계용 이벤트 한 건)에만 쓸 수 있습니다.

2. 큐: 사용자가 시작했지만 오래 걸리는 것

작업을 저장소에 기록하고, 별도의 워커가 꺼내서 처리하는 방식입니다. 요청은 "접수했다"만 응답하고 바로 끝납니다.

큐가 주는 것은 세 가지입니다. 작업이 기록되므로 유실되지 않고, 실패하면 다시 시도할 수 있고, 동시에 몇 개를 처리할지 통제할 수 있습니다. 외부 API를 호출하는 작업이라면 세 번째가 특히 중요합니다. 요청이 몰릴 때 상대 API의 한도를 넘기지 않으려면 처리 속도를 우리가 정해야 합니다.

전용 큐 서비스를 도입하지 않아도 데이터베이스 테이블로 시작할 수 있습니다.

CREATE TABLE jobs (
  id          bigserial PRIMARY KEY,
  kind        text NOT NULL,
  payload     jsonb NOT NULL,
  run_after   timestamptz NOT NULL DEFAULT now(),  -- 지연 실행·백오프용
  attempts    int NOT NULL DEFAULT 0,
  locked_at   timestamptz,
  finished_at timestamptz
);

워커가 작업을 집을 때는 여러 워커가 같은 행을 가져가지 않도록 해야 합니다. PostgreSQL이라면 잠긴 행을 건너뛰고 집는 방식이 편합니다.

SELECT * FROM jobs
WHERE finished_at IS NULL AND run_after <= now()
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;

재시도는 즉시 다시 하지 말고 간격을 늘려가며(지수 백오프) 시도하고, 일정 횟수를 넘기면 더 시도하지 않고 따로 모아둡니다. 무한 재시도는 장애를 키웁니다. 상대 서버가 죽어 있을 때 계속 두드리면 복구를 방해합니다.

진행 상황은 작업 ID를 응답으로 주고 클라이언트가 상태를 조회하게 하면 됩니다. 몇 초 간격 폴링으로 대부분 충분하고, 실시간성이 필요하면 서버가 진행률을 흘려보내는 방식으로 바꿉니다.

3. 크론: 시각이 트리거인 것

특정 시각이나 주기에 도는 작업입니다. 어제 데이터 집계, 만료된 세션 정리, 매일 발송하는 요약 메일이 여기 해당합니다.

사용자 작업을 크론으로 처리하려는 시도가 흔한 실수입니다. 5분마다 도는 크론에 맡기면 사용자는 최악의 경우 5분을 기다립니다. 주기를 줄이면 빈 실행이 늘어날 뿐입니다. 사용자가 시작한 일은 큐로 보내는 게 맞습니다.

크론에서 반드시 챙겨야 할 것이 두 가지입니다.

겹쳐 도는 것을 막아야 합니다. 이전 실행이 아직 안 끝났는데 다음 주기가 시작되면 같은 데이터를 두 번 처리합니다. 실행 전에 잠금을 잡거나, 애초에 여러 번 돌아도 결과가 같도록 작업을 설계해야 합니다.

돌지 않은 것을 알아챌 수 있어야 합니다. 크론은 조용히 멈춥니다. 서버가 죽었거나, 스케줄 등록이 지워졌거나, 스크립트가 오류로 종료돼도 아무 알림이 없습니다. 마지막 성공 시각을 기록해두고, 예상보다 오래 갱신되지 않으면 알림을 보내는 감시가 필요합니다. 이건 크론 자체가 아니라 별도 경로로 확인해야 의미가 있습니다.

서버리스에 배포했다면 플랫폼이 제공하는 스케줄러를 쓰거나, 외부에서 특정 엔드포인트를 주기적으로 호출하는 방식이 됩니다. 후자라면 그 엔드포인트에 반드시 인증을 걸어야 합니다. 주소만 알면 누구나 배치를 돌릴 수 있게 됩니다.

정리

작업 방식
폼 저장, 검색, 단순 조회 즉시 실행
가입 환영 메일, 알림 발송 큐
이미지·영상 변환, 파일 처리 큐
LLM 호출로 문서 생성 큐 (진행률은 폴링이나 스트리밍)
외부 API 대량 호출 큐 (동시 실행 수 제한)
일일 집계, 리포트 생성 크론
만료 데이터 정리 크론
요약·알림 메일 정기 발송 크론

큐를 쓰려면 별도 서버가 꼭 필요한가요?

아닙니다. 데이터베이스 테이블 하나로 시작할 수 있고, 하루에 처리하는 작업이 수천 건 수준이라면 그것으로 충분합니다. 전용 큐 서비스가 필요해지는 시점은 처리량이 많아 데이터베이스 부하가 문제가 되거나, 우선순위·지연 큐·재처리 같은 기능을 직접 만드는 비용이 커질 때입니다. 다만 워커를 돌릴 상시 프로세스는 어떤 형태로든 필요합니다. 서버리스만 쓰고 있다면 스케줄러가 주기적으로 워커 엔드포인트를 호출해 대기 중인 작업을 처리하게 만드는 방식으로 대체할 수 있습니다.

작업이 두 번 실행되는 건 어떻게 막나요?

완전히 막기보다 두 번 실행돼도 결과가 같게 만드는 편이 현실적입니다. 재시도가 있는 한 중복 실행 가능성은 남기 때문입니다. 작업마다 고유 키를 두고 이미 처리한 키인지 확인하는 방식이 기본입니다. 메일 발송이나 결제처럼 되돌릴 수 없는 작업일수록 이 설계가 중요합니다.

크론 주기는 어떻게 정하나요?

작업이 감당할 수 있는 지연 시간에서 거꾸로 잡습니다. 하루 한 번 보내는 요약이면 하루 주기로 충분하고, 주기를 줄인다고 나아지지 않습니다. 반대로 주기를 늘려도 되는데 짧게 잡아두면 빈 실행만 쌓입니다. 그리고 한 번 실행이 주기보다 오래 걸릴 가능성이 있다면 주기부터 늘리는 게 아니라 겹침 방지를 먼저 넣어야 합니다.

구조는 나중에 바꾸기 어렵다

세 방식의 차이는 코드 몇 줄이 아니라 실패했을 때 무슨 일이 일어나느냐입니다. 즉시 실행으로 만들어둔 것을 나중에 큐로 옮기려면 호출부와 결과 표시까지 함께 바꿔야 해서, 처음에 한 번 정하는 편이 쌉니다.

만든 프로젝트가 배포된 상태라면 Vibeollio에 프로젝트 등록해서 공개해보세요. 백그라운드 작업은 사용자가 몰리는 순간에 처음 문제가 드러나는데, 실제 방문자가 써보는 것만큼 그걸 빨리 확인시켜 주는 방법이 없습니다.

큐 vs 크론 vs 즉시 실행: 오래 걸리는 작업을 어디로 보낼까 | Vibeollio