사전 작업 큐
패턴

작업 큐

gabury1고친 사람 github-actions[bot]

작업 큐는 지금 당장 하지 않아도 되는 일을 적어 두었다가 다른 프로그램이 나중에 처리하게 넘겨 줍니다. 일을 맡기는 쪽은 큐에 한 줄을 넣고 바로 제 할 일로 돌아갑니다. 워커라고 부르는 처리 쪽이 큐에서 일을 하나씩 꺼내 끝냅니다.

자바스크립트에서는 실행을 기다리는 콜백의 줄을 같은 이름으로 부릅니다. 이 뜻은 상세 끝 소절에서 따로 다룹니다.

쉽고 빠른 이해

작업 큐는 오래 걸리는 일을 뒤로 미뤄 따로 처리하게 하는 장치입니다. 회원 가입을 받은 서버가 환영 메일 보내기를 큐에 넣고, 사용자에게는 바로 가입 완료를 알리는 것이 그런 예입니다.

이게 없으면 사용자는 메일이 나갈 때까지 화면 앞에서 기다립니다. 메일 서버가 느리거나 죽어 있으면 가입까지 같이 늦어지거나 실패합니다.

어떻게 도나:

  1. 일을 맡기는 쪽이 할 일을 짧게 적어 큐에 넣습니다
  2. 워커 여럿이 큐를 지켜보다가 하나씩 꺼내 갑니다. 일 하나는 워커 하나만 가져갑니다
  3. 워커는 일을 끝낸 뒤에야 끝났다고 알립니다. 알리기 전에 죽으면 그 일은 큐로 돌아옵니다

대가는 결과를 바로 받지 못한다는 점입니다. 같은 일이 두 번 실행될 수 있어서 두 번 돌아도 괜찮게 짜야 합니다. 큐와 워커를 따로 띄우고 지켜봐야 합니다.

상세

이 절은 환영 메일 하나가 큐에 들어가서 발송될 때까지를 따라갑니다. 그 길에서 작업 큐를 이루는 세 부분과, 워커가 도중에 죽어도 일이 살아남는 방법을 봅니다. 끝으로 큐가 줄지 않을 때와 이 방식을 안 쓰는 때를 가릅니다.

요청 경로에서 일을 떼어 내는 이유

세탁소 손님은 옷을 맡기고 영수증을 받으면 바로 가게를 나섭니다. 접수 직원은 맡은 옷을 바구니에 넣어 두고 다음 손님을 받습니다. 세탁 담당은 바구니에서 옷을 하나씩 꺼내 빱니다.

이 비유에서 손님은 사용자, 접수 직원은 웹 서버입니다. 바구니는 큐, 세탁 담당은 워커입니다. 손님이 세탁을 기다리지 않듯 사용자도 오래 걸리는 일을 기다리지 않게 만드는 것이 작업 큐입니다.

요청 경로는 사용자가 응답을 기다리는 동안 서버에서 도는 코드입니다. 이 코드가 오래 걸리면 사용자도 그만큼 기다립니다. 회원 가입 처리 안에서 환영 메일까지 보내면, 메일 서버가 답할 때까지 가입 응답이 나가지 않습니다.

환영 메일은 가입보다 몇 초 늦게 도착해도 아무도 곤란하지 않습니다. 이렇게 응답 전에 끝낼 필요가 없는 일을 요청 경로 밖으로 빼는 것이 작업 큐의 첫 목적입니다. 이미지 크기 줄이기, 보고서 만들기, 외부 서비스에 알림 보내기가 흔히 빠지는 일입니다.

큐에 넣는 할 일 한 건을 일감이라고 부릅니다. 아래 그림은 가입 요청 하나와 그 요청이 남긴 일감 하나를 따라갑니다.

sequenceDiagram
    participant U as 사용자
    participant S as 웹 서버
    participant Q as 큐
    participant W as 워커
    U->>S: 가입 요청
    S->>Q: 환영 메일 일감을 넣는다
    S-->>U: 가입 완료
    W->>Q: 다음 일감을 달라
    Q-->>W: 환영 메일 일감
    Note over W: 메일을 보낸다

그림에서 사용자는 세 번째 화살표에서 이미 응답을 받았습니다. 워커가 일감을 받아 가는 것은 그 뒤입니다. 메일이 늦게 나가도 가입 응답은 늦어지지 않습니다.

두 번째 목적은 실패를 떼어 놓는 것입니다. 메일 서버가 잠깐 죽어 있어도 가입은 성공합니다. 메일 보내기는 큐에 남아 있다가 메일 서버가 살아나면 다시 시도됩니다.

작업 큐를 이루는 세 부분

작업 큐는 세 부분으로 되어 있습니다. 일을 넣는 쪽, 일을 담아 두는 큐, 일을 꺼내 처리하는 쪽입니다.

부분 하는 일 환영 메일의 경우
생산자 할 일을 적어 큐에 넣는다 가입을 처리한 웹 서버
큐 넣은 순서대로 일을 담아 둔다 메일 보내기 일감이 줄을 선다
워커 큐에서 일을 꺼내 처리한다 메일 서버에 메일을 보내는 프로그램

표 가운데 행의 큐는 자료구조 큐와 같습니다. 먼저 넣은 일이 먼저 나옵니다. 생산자와 워커는 서로를 모릅니다. 둘 다 큐만 압니다.

큐에 넣는 일감 하나는 짧은 데이터입니다. 무슨 일인지와 그 일에 필요한 값만 담습니다. 환영 메일 일감은 이렇게 생겼습니다.

JSON
{"job": "welcome_mail", "user_id": 42}

메일 본문이나 사용자 정보 전체를 넣지 않고 사용자 번호만 넣었습니다. 워커는 일감을 꺼낸 뒤 그 번호로 필요한 것을 다시 읽어 옵니다. 일감이 큐에서 기다리는 동안 사용자가 이메일 주소를 바꿨어도 워커는 새 주소로 보냅니다.

생산자와 워커가 한 프로그램 안에 있으면 큐는 메모리 위의 자료구조 하나로 충분합니다. 스레드 풀이 안에 품은 큐가 이런 작업 큐입니다. 메모리 위의 큐는 프로그램이 죽으면 담긴 일도 같이 사라집니다.

서버 여러 대가 일을 주고받으려면 큐를 별도 서버에 둡니다. 프로그램 사이에서 메시지를 줄 세워 건네주는 이런 서버를 메시지 큐라고 부릅니다. 큐가 따로 있으면 웹 서버나 워커가 다시 시작해도 일감이 남습니다.

워커가 일을 나눠 가져가는 방식

워커는 여럿 띄우는 것이 보통입니다. 여러 워커가 한 큐를 같이 봅니다. 일감 하나는 그중 한 워커에게만 갑니다. 같은 메일을 두 워커가 동시에 보내지 않게 하려는 것입니다.

이렇게 한 큐를 두고 워커끼리 일을 다투어 가져가는 방식을 경쟁 소비자라고 부릅니다. 일이 많아지면 워커를 더 띄웁니다. 생산자는 손대지 않아도 됩니다.

flowchart TD
    subgraph P["생산자"]
        A1["웹 서버 1"]
        A2["웹 서버 2"]
    end
    Q["큐 · 넣은 순서대로"]
    subgraph W["워커 · 일감 하나는 워커 하나만"]
        W1["워커 1"]
        W2["워커 2"]
        W3["워커 3"]
    end
    A1 --> Q
    A2 --> Q
    Q --> W1
    Q --> W2
    Q --> W3

그림에서 웹 서버의 화살표는 큐에서 끝납니다. 워커를 셋에서 다섯으로 늘려도 웹 서버 쪽 화살표는 바뀌지 않습니다.

워커는 제가 감당할 만큼만 일을 받습니다. 하나를 끝내야 다음 것을 가져갑니다. 이 덕분에 몰려드는 일이 워커를 한꺼번에 덮치지 않습니다. 가입이 한순간에 천 건 몰려도 워커는 평소 속도대로 처리합니다.

넘치는 일은 큐에 쌓였다가 한가해지면 줄어듭니다. 큐가 몰림을 받아 두는 완충 역할을 합니다.

큐가 비어 있을 때 워커가 기다리는 방법은 둘입니다. 하나는 잠시 쉬었다가 다시 묻는 방법으로, 폴링이라고 합니다. 다른 하나는 큐와의 연결을 열어 둔 채 일이 들어올 때까지 답을 기다리는 방법입니다.

떼어 낸 대가

일을 요청 경로에서 떼어 내면 잃는 것이 셋 있습니다. 결과, 순서, 그리고 단순함입니다. 같은 일이 두 번 실행되는 문제는 아래 「워커가 도중에 죽을 때」에서 따로 봅니다.

먼저 결과를 바로 받지 못합니다. 생산자는 일을 넣은 순간 응답을 돌려주므로 그 일이 성공했는지 모릅니다. 사용자에게 결과를 보여 줘야 하면 워커가 결과를 어딘가에 적어 둡니다. 사용자 쪽은 끝났는지 다시 물어야 합니다.

다음으로 순서가 흐트러집니다. 큐는 넣은 순서대로 꺼내 주지만 워커가 여럿이면 끝나는 순서는 제각각입니다. 먼저 꺼낸 일이 오래 걸리면 뒤에 꺼낸 일이 먼저 끝납니다.

마지막으로 굴릴 것이 늘어납니다. 큐를 담는 서버와 워커 프로그램을 따로 띄우고 지켜봐야 합니다. 일이 어디서 멈췄는지 찾을 때도 웹 서버 기록만 봐서는 부족합니다.

워커가 도중에 죽을 때

워커가 일감을 꺼내 가자마자 큐에서 지우면 곤란해집니다. 메일을 보내던 워커가 도중에 죽으면 그 일은 어디에도 남지 않습니다. 사용자는 환영 메일을 영영 못 받습니다.

그래서 큐는 일감을 내준 뒤에도 바로 지우지 않고 처리 중으로 표시해 둡니다. 워커가 일을 끝내고 끝났다고 알려야 그때 지웁니다. 이 알림이 확인 응답(ack, acknowledgment)입니다. 정해진 시간 안에 확인 응답이 안 오면 큐는 그 워커가 죽었다고 보고 일감을 대기로 되돌립니다.

어떤 일은 몇 번을 다시 해도 실패합니다. 메일 주소가 잘못된 경우가 그렇습니다. 이런 일을 끝없이 되돌리면 워커가 같은 실패만 되풀이합니다.

이 되풀이를 막으려고 재시도 횟수에 상한을 둡니다. 상한을 넘긴 일감은 따로 떼어 별도 큐에 모읍니다. 이 큐를 데드 레터 큐라고 부릅니다. 사람이 나중에 들여다보고 고쳐서 다시 넣거나 버립니다.

아래 그림은 일감 하나가 거치는 상태를 한데 모았습니다.

stateDiagram-v2
    state "처리 중" as 처리
    state "데드 레터 큐" as 보관
    [*] --> 대기: 생산자가 넣음
    대기 --> 처리: 워커가 꺼냄
    처리 --> [*]: 확인 응답 · 지움
    처리 --> 대기: 시간 초과 · 다시 시도
    처리 --> 보관: 정해 둔 횟수만큼 실패

다시 시도하는 간격은 보통 조금씩 늘립니다. 잠깐 죽은 메일 서버가 살아날 틈을 주려는 것입니다. 기다리는 시간을 매번 두 배씩 늘리는 방식이 지수 백오프입니다.

확인 응답에도 빈틈이 하나 남습니다. 워커가 메일을 보낸 직후, 확인 응답을 보내기 직전에 죽는 경우입니다.

sequenceDiagram
    participant Q as 큐
    participant W1 as 워커 1
    participant W2 as 워커 2
    participant M as 메일 서버
    Q->>W1: 환영 메일 일감
    W1->>M: 메일을 보낸다
    Note over W1: 확인 응답 전에 죽는다
    Note over Q: 시간 초과 · 대기로 되돌린다
    Q->>W2: 같은 일감
    W2->>M: 메일을 또 보낸다

큐는 일이 안 끝났다고 보고 다른 워커에게 다시 줍니다. 사용자는 환영 메일을 두 통 받습니다.

이처럼 작업 큐는 일감을 빠뜨리지 않는 대신 두 번 내줄 수 있습니다. 빠짐없이 전하되 두 번 전할 수도 있는 이 방식을 적어도 한 번 전달이라고 부릅니다. 작업 큐의 일은 이것을 전제로 짭니다.

그래서 일을 짤 때는 같은 일이 두 번 돌아도 결과가 한 번 돈 것과 같게 만듭니다. 이런 성질을 멱등성이라고 합니다. 메일이라면 보낸 기록을 남기고, 이미 보낸 사용자는 건너뛰게 합니다.

큐가 줄지 않을 때

일이 들어오는 속도가 워커가 처리하는 속도보다 계속 빠르면 큐가 끝없이 길어집니다. 이 상태가 큐 적체입니다. 쌓인 일은 점점 늦게 처리됩니다. 끝내는 큐를 담은 서버의 메모리나 디스크가 찹니다.

작업 큐를 굴릴 때는 큐 길이를 지켜봅니다. 가장 오래 기다린 일감이 몇 분째 기다리는지도 함께 봅니다. 길이는 얼마나 밀렸는지를, 기다린 시간은 사용자가 결과를 얼마나 늦게 받는지를 알려 줍니다.

쌓이는 것을 막는 길은 둘입니다. 워커를 늘려 처리 속도를 올리거나, 넣는 쪽을 늦춥니다. 큐 길이에 상한을 두고 넘치면 새 일을 거절하거나 넣는 쪽을 기다리게 하는 조절이 백프레셔입니다.

큐 하나에 성격이 다른 일을 섞어도 막힙니다. 몇 분 걸리는 보고서 만들기가 워커를 전부 붙잡으면, 1초면 끝날 메일 보내기가 그 뒤에서 기다립니다. 앞에 선 일 때문에 뒤의 일이 못 나아가는 이 현상이 헤드 오브 라인 블로킹입니다.

그래서 오래 걸리는 일과 빨리 끝나는 일은 큐를 나누고 워커도 따로 둡니다. 급한 일을 먼저 꺼내 주는 우선순위 큐를 쓰기도 합니다.

메시지 큐와 가르는 선

작업 큐는 흔히 메시지 큐 위에 만듭니다. 메시지 큐는 앞에서 본 대로 프로그램 사이에서 메시지를 줄 세워 건네주는 서버입니다. 이 서버가 프로그램 사이의 통로 구실을 합니다.

작업 큐는 그 통로를 쓰는 한 방식입니다. 메시지 하나하나가 해야 할 일입니다. 생산자가 일을 넣고, 그 일을 워커 하나만 맡아 처리합니다. 앞에서 본 세 부분의 짜임이 곧 이 방식입니다.

같은 통로를 다르게 쓰기도 합니다. 메시지 하나를 받는 쪽 모두에게 한 부씩 나눠 주는 방식이 발행-구독입니다. 가입 소식을 메일 서비스와 통계 서비스가 각자 받아야 한다면 이쪽입니다. 작업 큐에서는 일 하나가 한 번만 처리되어야 하므로 한 부만 나갑니다.

자바스크립트의 작업 큐

자바스크립트에서 작업 큐는 다른 것을 가리킵니다. 실행할 차례를 기다리는 콜백을 줄 세워 둔 큐입니다. 콜백은 어떤 일이 끝나면 불러 달라고 미리 넘겨 둔 함수입니다.

타이머가 끝나거나 네트워크 응답이 오면 그 콜백이 이 줄에 섭니다. 이벤트 루프는 이 줄에서 콜백을 하나씩 꺼내 실행하기를 되풀이하는 장치입니다. 꺼내 실행하는 쪽은 워커 여럿이 아니라 코드를 실행하는 흐름 하나, 곧 스레드 하나입니다. 이 편에서 다룬 작업 큐와는 일을 줄 세워 두고 차례로 꺼낸다는 뼈대만 같습니다.

쓰는 때와 안 쓰는 때

응답 전에 끝낼 필요가 없고 시간이 걸리는 일에 씁니다. 메일 서버에 메일을 보내는 일처럼 실패해서 다시 해야 할 수 있는 일에도 맞습니다. 요청이 몰리는 때와 한가한 때의 차이가 큰 서비스라면 큐가 그 차이를 받아 줍니다.

사용자가 결과를 보고 다음으로 넘어가야 하는 일에는 안 맞습니다. 로그인 확인이나 잔액 조회를 큐에 넣으면 응답을 기다리게 할 방법만 복잡해집니다. 금방 끝나는 일은 큐에 넣고 꺼내는 비용이 일 자체보다 커지기도 합니다.

정해진 시각에 한꺼번에 도는 일은 작업 큐보다 배치 처리와 크론 쪽입니다. 둘을 같이 쓰기도 합니다. 크론이 밤마다 일감을 만들어 큐에 넣고 워커가 나눠 처리하는 식입니다.

flowchart TD
    A["사용자가 응답 전에 결과를 봐야 하나"]
    A -- 예 --> B["요청 경로에서 바로 처리"]
    A -- 아니오 --> C["정해진 시각에 모아서 도나"]
    C -- 예 --> D["배치 처리 · 크론"]
    C -- 아니오 --> E["작업 큐"]

관련 항목

작업 큐를 이루는 구성 요소

큐 · 생산자 · 워커 · 메시지 · 확인 응답 · 소비자

작업 큐가 일감을 담아 두는 저장소

메시지 큐 · 메시지 브로커 · 스레드 풀 · 유계 큐 · 인메모리 큐

작업 큐를 구현·채택한 제품

Redis · Amazon SQS · RabbitMQ · Celery · Sidekiq · BullMQ

워커가 큐에서 일감을 받아 오는 방식

폴링 · 롱 폴링 · 경쟁 소비자 · 프리페치

작업 큐에서 자주 나는 장애

큐 적체 · 헤드 오브 라인 블로킹 · 포이즌 메시지 · 기아 · 중복 전달

작업 큐의 실패를 견디게 하는 수단

재시도 · 지수 백오프 · 멱등성 · 데드 레터 큐 · 적어도 한 번 전달 · 정확히 한 번 전달

작업 큐로 들어오는 일의 양을 조절하는 수단

백프레셔 · 속도 제한 · 우선순위 큐 · 부하 평준화

작업 큐를 재는 지표

처리량 · 지연 시간 · 큐 길이 · 리틀의 법칙 · 큐잉 이론

작업 큐 대신 고르거나 함께 쓰는 처리 방식

배치 처리 · 크론 · 스케줄러 · 발행-구독 · 동기 처리

작업 큐라는 이름을 같이 쓰는 이벤트 루프 구조

이벤트 루프 · 콜백 · 태스크 큐 · 마이크로태스크 큐 · 호출 스택

작업 큐가 속하는 상위 분류

비동기 처리 · 이벤트 기반 아키텍처 · 분산 시스템 · 메시징 · 백엔드

다른 이름: job queue · task queue · work queue · 잡 큐 · 일감 큐