🔧 DevOps

Worker

워커

백그라운드 작업 처리 프로세스. Kubernetes 워커 노드.

상세 설명

Worker는 백그라운드에서 작업을 처리하는 프로세스입니다. 웹 요청-응답 사이클과 분리되어, 이메일 발송, 이미지 처리, 데이터 집계 같은 시간이 오래 걸리거나 즉각적인 응답이 필요 없는 작업을 비동기로 처리합니다.

메시지 큐 기반 아키텍처에서 Producer가 작업을 큐에 넣으면, Worker가 이를 소비(consume)해 처리합니다. RabbitMQ, Redis Queue, Amazon SQS, Kafka가 대표적인 메시지 브로커입니다. Celery(Python), BullMQ(Node.js), Sidekiq(Ruby)가 Worker 프레임워크입니다.

Kubernetes에서 Worker는 Deployment로 배포되며 Horizontal Pod Autoscaler로 큐 길이에 따라 자동 스케일됩니다. 컨트롤 플레인의 명령을 수행하는 노드도 Worker Node라고 부르며, 이는 Pod를 실행하는 서버를 의미합니다.

실무에서는 작업 실패 시 재시도 로직(exponential backoff), Dead Letter Queue(처리 실패 작업 격리), 멱등성(idempotency, 중복 실행해도 결과 동일) 보장이 중요합니다. 모니터링으로 큐 길이, 처리 시간, 실패율을 추적합니다.

코드 예제

// BullMQ를 사용한 Node.js Worker 예제

// queue.js - 큐 설정
import { Queue } from 'bullmq';

const connection = {
  host: process.env.REDIS_HOST || 'localhost',
  port: 6379,
};

// 이메일 발송 큐
export const emailQueue = new Queue('email', { connection });

// 이미지 처리 큐
export const imageQueue = new Queue('image-processing', {
  connection,
  defaultJobOptions: {
    attempts: 3,                    // 최대 3번 재시도
    backoff: {
      type: 'exponential',          // 지수 백오프
      delay: 1000,                  // 1초부터 시작
    },
    removeOnComplete: 100,          // 완료된 작업 100개만 유지
    removeOnFail: 500,              // 실패한 작업 500개만 유지
  },
});

// producer.js - 작업 추가
export async function sendWelcomeEmail(userId, email) {
  const job = await emailQueue.add('welcome', {
    userId,
    email,
    template: 'welcome',
  }, {
    priority: 1,                    // 높은 우선순위
    delay: 0,                       // 즉시 실행
    jobId: `welcome-${userId}`,     // 중복 방지용 ID
  });

  console.log(`Job ${job.id} added to queue`);
  return job;
}

export async function processImage(imageId, operations) {
  return imageQueue.add('resize', {
    imageId,
    operations,  // [{ type: 'resize', width: 800 }, { type: 'compress' }]
  });
}

// worker.js - Worker 정의
import { Worker } from 'bullmq';
import { sendEmail } from './services/email';
import { resizeImage, compressImage } from './services/image';

// 이메일 Worker
const emailWorker = new Worker('email', async (job) => {
  console.log(`Processing email job ${job.id}`);

  const { userId, email, template } = job.data;

  // 멱등성: 이미 발송된 이메일인지 확인
  if (await isEmailAlreadySent(userId, template)) {
    console.log(`Email already sent to ${email}`);
    return { status: 'skipped', reason: 'already_sent' };
  }

  await sendEmail({
    to: email,
    template,
    data: { userId },
  });

  await markEmailAsSent(userId, template);

  return { status: 'sent', email };
}, {
  connection,
  concurrency: 5,  // 동시 처리 수
});

// 이벤트 핸들링
emailWorker.on('completed', (job, result) => {
  console.log(`Job ${job.id} completed:`, result);
});

emailWorker.on('failed', (job, err) => {
  console.error(`Job ${job.id} failed:`, err.message);
  // 알림 발송, 메트릭 기록 등
});

// 이미지 처리 Worker
const imageWorker = new Worker('image-processing', async (job) => {
  const { imageId, operations } = job.data;

  let image = await loadImage(imageId);

  for (const op of operations) {
    // 진행률 업데이트
    await job.updateProgress(operations.indexOf(op) / operations.length * 100);

    if (op.type === 'resize') {
      image = await resizeImage(image, op.width, op.height);
    } else if (op.type === 'compress') {
      image = await compressImage(image, op.quality);
    }
  }

  const outputPath = await saveImage(image);
  return { outputPath };
}, {
  connection,
  concurrency: 2,  // 리소스 많이 쓰므로 제한
});

// Kubernetes Deployment (worker.yaml)
/*
apiVersion: apps/v1
kind: Deployment
metadata:
  name: email-worker
spec:
  replicas: 3
  selector:
    matchLabels:
      app: email-worker
  template:
    spec:
      containers:
      - name: worker
        image: myapp/worker:v1.0.0
        command: ["node", "worker.js"]
        env:
        - name: REDIS_HOST
          valueFrom:
            configMapKeyRef:
              name: redis-config
              key: host
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
*/

실무에서 이렇게 말해요

시니어: "주문 완료 이메일이 API 응답 시간에 영향주고 있어요. Worker로 분리해서 비동기 처리해야 해요."

주니어: "큐에 넣었는데 Worker가 죽으면 이메일이 안 가지 않나요?"

시니어: "Redis persistence 설정하고, 작업 실패 시 재시도 로직 넣으면 돼요. Dead Letter Queue로 계속 실패하는 건 따로 모아서 확인하고요."

면접관: "메시지 큐 기반 시스템에서 멱등성을 어떻게 보장하나요?"

지원자: "작업마다 고유 ID를 부여하고, 처리 완료된 ID는 Redis나 DB에 기록합니다. Worker가 작업을 받으면 먼저 이 ID로 이미 처리됐는지 확인하고, 중복이면 스킵합니다. 네트워크 문제로 재전송되거나 재시도되어도 같은 결과를 보장합니다."

리뷰어: "외부 API 호출하는 Worker인데 타임아웃이나 재시도 설정이 없네요."

개발자: "axios timeout 30초로 설정하고, BullMQ에서 attempts: 3, exponential backoff 추가하겠습니다. 3번 실패하면 DLQ로 가게요."

주의사항

더 배우기