드루팔 Queue API 완전정복: 대규모 배치 작업을 효율적으로 처리하는 방법 🚀

콘텐츠 대표 이미지 - 드루팔 Queue API 완전정복: 대규모 배치 작업을 효율적으로 처리하는 방법 🚀

 

 

2025년 3월 3일 기준 최신 정보

🌟 드루팔 Queue API란 뭐야? 친구처럼 쉽게 설명해줄게!

안녕! 오늘은 드루팔의 숨은 영웅인 Queue API에 대해 함께 알아볼 거야. 웹사이트를 운영하다 보면 대용량 데이터를 처리하거나 시간이 오래 걸리는 작업을 해야 할 때가 있잖아? 이럴 때 사용자가 "로딩 중..." 화면을 보며 지루하게 기다리게 하는 건 최악의 경험이지! 😱

바로 이런 상황에서 Queue API가 우리의 구원자로 등장해. 간단히 말하면, Queue API는 시간이 오래 걸리는 작업들을 "대기열"에 넣어두고 백그라운드에서 차근차근 처리해주는 드루팔의 강력한 기능이야. 마치 카페에서 주문을 받고 번호표를 나눠주는 것처럼, 작업을 접수받고 순서대로 처리하는 시스템이지! ☕

드루팔 Queue API 작동 방식 사용자 드루팔 웹사이트 Queue API Worker 백그라운드 처리 1. 작업 요청 2. 큐에 작업 추가 3. 백그라운드 처리

웹 개발자라면 누구나 공감할 거야. 이미지 수천 장을 처리하거나, 대량의 데이터를 마이그레이션하거나, 복잡한 보고서를 생성할 때 사용자가 브라우저에서 "처리 중..." 화면을 보며 기다리게 하는 건 최악의 UX지. Queue API를 사용하면 이런 작업들을 백그라운드로 옮겨서 사용자는 다른 일을 할 수 있게 해주고, 작업이 완료되면 알려줄 수 있어. 👍

🔍 왜 Queue API가 필요한 걸까?

웹사이트를 운영하다 보면 다음과 같은 상황에 자주 부딪히게 돼:

  1. 시간이 오래 걸리는 작업 - 대용량 파일 처리, 데이터 마이그레이션 등
  2. 주기적으로 실행해야 하는 작업 - 데이터 정리, 백업, 보고서 생성
  3. 외부 API와의 통신 - 응답이 느린 외부 서비스와 통신할 때
  4. 리소스를 많이 사용하는 작업 - 서버에 부하를 줄 수 있는 무거운 작업
  5. 실패 시 재시도가 필요한 작업 - 네트워크 오류 등으로 실패할 수 있는 작업

이런 작업들을 그냥 웹 요청으로 처리하면 어떤 문제가 생길까? 🤔

🚫 Queue 없이 처리할 때의 문제점

1. 타임아웃 발생 - PHP의 기본 실행 시간 제한(보통 30초)을 초과하면 작업이 중단돼.

2. 메모리 부족 - 대용량 데이터 처리 시 메모리 한계에 도달할 수 있어.

3. 사용자 경험 저하 - 사용자가 작업이 완료될 때까지 기다려야 해.

4. 서버 부하 증가 - 동시에 여러 무거운 작업이 실행되면 서버가 과부하될 수 있어.

5. 작업 추적 어려움 - 작업 상태나 진행 상황을 추적하기 어려워.

이런 문제들을 해결하기 위해 드루팔은 Queue API를 제공해. 이 API를 사용하면 작업을 대기열에 넣고, 백그라운드에서 처리하고, 실패 시 재시도하고, 작업 상태를 추적할 수 있어. 특히 재능넷과 같은 사용자 참여형 플랫폼에서는 대량의 데이터 처리나 알림 발송 같은 작업을 Queue API로 처리하면 사이트 성능을 크게 향상시킬 수 있어! 💪

🛠️ 드루팔 10의 Queue API 구조

드루팔 8부터 도입되고 드루팔 10에서 더욱 강화된 Queue API는 다음과 같은 주요 컴포넌트로 구성돼 있어:

Queue API 구조 QueueInterface QueueFactory QueueWorker DatabaseQueue MemoryQueue RedisQueue BeanstalkdQueue QueueItem

이 구조에서 각 컴포넌트의 역할을 살펴볼게:

  1. QueueInterface - 모든 큐 구현체가 따라야 하는 인터페이스야. 작업을 추가하고, 가져오고, 삭제하는 메서드를 정의해.
  2. QueueFactory - 다양한 큐 구현체를 생성하는 팩토리 클래스야. 설정에 따라 적절한 큐 구현체를 제공해.
  3. QueueWorker - 큐에서 작업을 가져와 실행하는 워커야. 작업 실행, 실패 처리, 재시도 등을 담당해.
  4. Queue 구현체 - 실제 큐 기능을 구현한 클래스들이야:
    1. DatabaseQueue - 드루팔 데이터베이스를 사용하는 기본 구현체
    2. MemoryQueue - 메모리에서 작동하는 경량 구현체 (주로 테스트용)
    3. RedisQueue - Redis를 백엔드로 사용하는 구현체
    4. BeanstalkdQueue - Beanstalkd 큐 서버를 사용하는 구현체
  5. QueueItem - 큐에 저장되는 작업 아이템이야. 실행할 콜백, 데이터, 태그 등을 포함해.

드루팔 10에서는 이 구조가 더욱 모듈화되고 확장 가능하게 개선되었어. 특히 다양한 백엔드를 쉽게 추가할 수 있는 플러그인 시스템이 강화되었지! 🔌

🚀 Queue API 시작하기: 기본 설정

자, 이제 실제로 Queue API를 사용해보자! 먼저 기본 설정부터 시작할게.

1️⃣ 필요한 모듈 설치하기

드루팔 10에서는 Queue API가 코어에 포함되어 있지만, 실제로 사용하려면 Queue 모듈을 설치해야 해:

composer require drupal/core-queue

추가적인 기능이 필요하다면 다음 모듈들도 고려해볼 수 있어:

  1. Advanced Queue - 더 많은 기능을 제공하는 확장 모듈
    composer require drupal/advancedqueue
  2. Queue UI - 큐 관리를 위한 UI를 제공하는 모듈
    composer require drupal/queue_ui
  3. Redis - Redis 백엔드를 사용하기 위한 모듈
    composer require drupal/redis

2️⃣ 큐 설정하기

기본적으로 드루팔은 데이터베이스를 큐 백엔드로 사용해. 하지만 settings.php 파일에서 다른 백엔드를 설정할 수 있어:

// Database 큐 사용 (기본값)
$settings['queue_default'] = 'database';

// Redis 큐 사용
$settings['queue_default'] = 'redis';
$settings['redis.connection']['host'] = '127.0.0.1';
$settings['redis.connection']['port'] = 6379;

// 특정 큐에 대해 다른 백엔드 사용
$settings['queue_service_myqueue'] = 'redis';

여기서 'myqueue'는 네가 정의할 큐의 이름이야. 여러 큐를 만들어서 각각 다른 용도로 사용할 수 있어! 🎯

3️⃣ Cron 설정하기

큐 작업을 주기적으로 처리하려면 Cron을 설정해야 해. 드루팔의 cron.php를 주기적으로 실행하도록 서버의 crontab을 설정하는 게 좋아:

*/15 * * * * wget -O - -q -t 1 http://example.com/cron.php?cron_key=YOUR_CRON_KEY

또는 Drush를 사용할 수도 있어:

*/15 * * * * cd /path/to/drupal && drush core:cron

큐 작업이 많거나 자주 실행해야 한다면 별도의 워커 프로세스를 설정하는 것이 좋아. 이건 나중에 더 자세히 설명할게! ⏱️

💡 Queue API 기본 사용법

이제 Queue API의 기본적인 사용법을 알아볼게. 크게 세 가지 단계로 나눌 수 있어:

  1. 큐 정의하기
  2. 큐에 작업 추가하기
  3. 큐 작업 처리하기

1️⃣ 큐 정의하기

먼저 모듈의 mymodule.services.yml 파일에 큐 서비스를 정의해:

services:
  mymodule.my_queue:
    class: Drupal\Core\Queue\QueueInterface
    factory: ['@queue', 'get']
    arguments: ['mymodule_queue_name']

이렇게 하면 'mymodule_queue_name'이라는 이름의 큐를 서비스로 등록하게 돼. 이제 이 서비스를 의존성 주입으로 가져와서 사용할 수 있어! 🧩

2️⃣ 큐에 작업 추가하기

이제 큐에 작업을 추가해보자. 다음은 컨트롤러나 서비스에서 큐에 작업을 추가하는 예시야:

/**
 * @param \Drupal\Core\Queue\QueueFactory $queue_factory
 */
public function addItemsToQueue(QueueFactory $queue_factory) {
  // 큐 가져오기
  $queue = $queue_factory->get('mymodule_queue_name');
  
  // 데이터 준비
  $data = [
    'user_id' => 123,
    'operation' => 'send_email',
    'params' => ['subject' => '안녕하세요!', 'body' => '큐 API 테스트입니다.'],
  ];
  
  // 큐에 아이템 추가
  $queue->createItem($data);
  
  return ['#markup' => '큐에 작업이 추가되었습니다!'];
}

의존성 주입을 사용하지 않는다면 다음과 같이 할 수도 있어:

// 서비스 컨테이너에서 큐 팩토리 가져오기
$queue_factory = \Drupal::service('queue');

// 큐 가져오기
$queue = $queue_factory->get('mymodule_queue_name');

// 큐에 아이템 추가
$queue->createItem($data);

$data에는 어떤 PHP 데이터든 넣을 수 있어. 단, 직렬화가 가능해야 하니 객체를 넣을 때는 주의해야 해! 🧐

3️⃣ 큐 작업 처리하기

큐에 추가한 작업을 처리하는 방법은 두 가지가 있어:

A. QueueWorker 플러그인 사용하기

가장 권장되는 방법은 QueueWorker 플러그인을 만드는 거야. 이 방법을 사용하면 드루팔이 cron 실행 시 자동으로 큐 작업을 처리해줘.

먼저 src/Plugin/QueueWorker 디렉토리에 플러그인 클래스를 만들어:

namespace Drupal\mymodule\Plugin\QueueWorker;

use Drupal\Core\Queue\QueueWorkerBase;

/**
 * 이메일 발송 큐 작업자.
 *
 * @QueueWorker(
 *   id = "mymodule_queue_name",
 *   title = @Translation("My Module Queue Worker"),
 *   cron = {"time" = 60}
 * )
 */
class MyQueueWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    // 큐 아이템 처리 로직
    \Drupal::logger('mymodule')->notice('큐 아이템 처리 중: @data', ['@data' => print_r($data, TRUE)]);
    
    // 예: 이메일 발송
    if ($data['operation'] === 'send_email') {
      $mailManager = \Drupal::service('plugin.manager.mail');
      $mailManager->mail(
        'mymodule',
        'notice',
        'user@example.com',
        'ko',
        [
          'subject' => $data['params']['subject'],
          'body' => $data['params']['body'],
        ]
      );
    }
  }
}

이 플러그인의 @QueueWorker 어노테이션에서 중요한 부분은:

  1. id - 처리할 큐의 이름 (위에서 정의한 큐 이름과 일치해야 함)
  2. title - 관리 UI에 표시될 제목
  3. cron - cron 실행 시 처리할 시간(초) 제한

이렇게 설정하면 드루팔이 cron 실행 시 자동으로 큐 아이템을 처리해줘. 각 cron 실행마다 최대 60초 동안 큐 아이템을 처리하게 돼. 👍

B. 수동으로 큐 처리하기

때로는 cron을 기다리지 않고 즉시 큐를 처리하고 싶을 수도 있어. 이럴 때는 다음과 같이 수동으로 처리할 수 있어:

// 큐와 워커 가져오기
$queue = \Drupal::queue('mymodule_queue_name');
$queue_manager = \Drupal::service('plugin.manager.queue_worker');
$queue_worker = $queue_manager->createInstance('mymodule_queue_name');

// 큐에서 아이템을 가져와 처리
while ($item = $queue->claimItem()) {
  try {
    $queue_worker->processItem($item->data);
    $queue->deleteItem($item);
  }
  catch (\Exception $e) {
    // 오류 발생 시 아이템 릴리스 (다시 처리할 수 있도록)
    $queue->releaseItem($item);
    watchdog_exception('mymodule', $e);
  }
}

이 방법은 Drush 명령어나 특정 관리 페이지에서 큐를 즉시 처리하고 싶을 때 유용해! 🚀

🔄 고급 Queue API 기능

기본 사용법을 마스터했다면 이제 더 고급 기능들을 살펴볼 차례야! 대규모 사이트에서는 이런 고급 기능들이 정말 유용하지. 특히 재능넷과 같은 활발한 사용자 참여 플랫폼에서는 이런 기능들이 사이트 성능을 크게 향상시킬 수 있어! 🚀

1️⃣ 재시도 메커니즘 구현하기

큐 작업이 실패할 경우 재시도하는 메커니즘을 구현하는 것이 중요해. 다음은 QueueWorker 플러그인에서 재시도 로직을 구현하는 예시야:

namespace Drupal\mymodule\Plugin\QueueWorker;

use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\Queue\RequeueException;
use Drupal\Core\Queue\SuspendQueueException;

/**
 * @QueueWorker(
 *   id = "mymodule_queue_name",
 *   title = @Translation("My Module Queue Worker"),
 *   cron = {"time" = 60}
 * )
 */
class MyQueueWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    // 재시도 횟수 확인
    $max_retries = 3;
    $retry_count = isset($data['_retry_count']) ? $data['_retry_count'] : 0;
    
    try {
      // 외부 API 호출 등 실패할 수 있는 작업
      $result = $this->callExternalApi($data);
      
      if (!$result) {
        throw new \Exception('외부 API 호출 실패');
      }
      
      // 성공적으로 처리됨
      \Drupal::logger('mymodule')->notice('작업 성공: @id', ['@id' => $data['id']]);
    }
    catch (\Exception $e) {
      // 재시도 횟수 증가
      $retry_count++;
      
      if ($retry_count <= $max_retries) {
        // 재시도를 위해 큐에 다시 추가
        $data['_retry_count'] = $retry_count;
        \Drupal::logger('mymodule')->warning('작업 실패, 재시도 @count/3: @id', [
          '@count' => $retry_count,
          '@id' => $data['id'],
        ]);
        
        // RequeueException을 던지면 아이템이 큐에 다시 추가됨
        throw new RequeueException('작업 실패, 재시도 예정');
      }
      else {
        // 최대 재시도 횟수 초과
        \Drupal::logger('mymodule')->error('최대 재시도 횟수 초과: @id', ['@id' => $data['id']]);
        
        // 오류 기록 후 다음 아이템으로 진행 (이 아이템은 삭제됨)
        // 또는 전체 큐를 일시 중지하려면 SuspendQueueException 사용
        // throw new SuspendQueueException('심각한 오류로 큐 처리 중단');
      }
    }
  }
  
  /**
   * 외부 API 호출 예시 메서드.
   */
  protected function callExternalApi($data) {
    // 실제 API 호출 로직
    // 성공 시 true, 실패 시 false 반환
    return rand(0, 10) > 3; // 70% 확률로 성공
  }
}

여기서 중요한 예외 클래스들은:

  1. RequeueException - 이 예외를 던지면 아이템이 큐에 다시 추가돼 (재시도)
  2. SuspendQueueException - 이 예외를 던지면 현재 cron 실행에서 이 큐의 처리가 중단돼 (심각한 오류 발생 시)

이런 재시도 메커니즘은 네트워크 오류나 일시적인 문제로 인한 실패를 우아하게 처리할 수 있게 해줘! 🔄

2️⃣ 배치 처리 최적화하기

대량의 아이템을 처리할 때는 배치 처리를 통해 성능을 최적화할 수 있어. 다음은 배치 처리를 구현하는 예시야:

/**
 * 배치 처리를 위한 큐 아이템 추가.
 */
function mymodule_add_batch_items() {
  $queue = \Drupal::queue('mymodule_batch_queue');
  $batch_size = 100;
  $total_items = 10000;
  
  // 전체 작업을 배치로 나누기
  for ($i = 0; $i < $total_items; $i += $batch_size) {
    $batch = [
      'start' => $i,
      'size' => min($batch_size, $total_items - $i),
      'total' => $total_items,
    ];
    $queue->createItem($batch);
  }
  
  return t('@count개의 배치 작업이 큐에 추가되었습니다.', ['@count' => ceil($total_items / $batch_size)]);
}

그리고 이를 처리하는 QueueWorker:

/**
 * 배치 처리 큐 워커.
 *
 * @QueueWorker(
 *   id = "mymodule_batch_queue",
 *   title = @Translation("Batch Processing Queue"),
 *   cron = {"time" = 180}
 * )
 */
class BatchQueueWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    $start = $data['start'];
    $size = $data['size'];
    $total = $data['total'];
    
    \Drupal::logger('mymodule')->notice('배치 처리 중: @start - @end / @total', [
      '@start' => $start + 1,
      '@end' => $start + $size,
      '@total' => $total,
    ]);
    
    // 배치 내의 아이템 처리
    for ($i = $start; $i < $start + $size; $i++) {
      // 각 아이템 처리 로직
      $this->processIndividualItem($i);
    }
    
    // 진행 상황 업데이트 (상태 저장소 사용)
    $state = \Drupal::state();
    $processed = $state->get('mymodule.batch_processed', 0) + $size;
    $state->set('mymodule.batch_processed', $processed);
    
    // 모든 배치가 완료되었는지 확인
    if ($processed >= $total) {
      \Drupal::logger('mymodule')->notice('모든 배치 처리 완료: @total 아이템', ['@total' => $total]);
      // 완료 후 정리 작업
      $this->finalizeBatchProcessing();
    }
  }
  
  /**
   * 개별 아이템 처리.
   */
  protected function processIndividualItem($index) {
    // 개별 아이템 처리 로직
  }
  
  /**
   * 배치 처리 완료 후 정리 작업.
   */
  protected function finalizeBatchProcessing() {
    // 완료 후 정리 작업
    $state = \Drupal::state();
    $state->delete('mymodule.batch_processed');
    
    // 완료 알림 등
  }
}

이 방식은 10,000개의 아이템을 100개씩 나눠서 처리하므로, 메모리 사용량을 크게 줄이고 각 배치가 독립적으로 처리되어 실패 시 영향 범위를 제한할 수 있어! 📊

3️⃣ 작업 진행 상황 추적하기

큰 작업의 진행 상황을 추적하고 사용자에게 보여주는 것은 좋은 UX를 위해 중요해. 다음은 진행 상황을 추적하는 방법이야:

/**
 * 진행 상황을 추적하는 큐 워커.
 */
class ProgressTrackingWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    // State API를 사용하여 진행 상황 추적
    $state = \Drupal::state();
    $progress_key = 'mymodule.import_progress';
    
    // 현재 진행 상황 가져오기
    $progress = $state->get($progress_key, [
      'total' => $data['total'],
      'processed' => 0,
      'success' => 0,
      'failed' => 0,
      'last_update' => 0,
    ]);
    
    try {
      // 아이템 처리
      $this->processImportItem($data);
      
      // 성공 카운트 증가
      $progress['processed']++;
      $progress['success']++;
    }
    catch (\Exception $e) {
      // 실패 카운트 증가
      $progress['processed']++;
      $progress['failed']++;
      \Drupal::logger('mymodule')->error('아이템 처리 실패: @message', ['@message' => $e->getMessage()]);
    }
    
    // 진행률 계산
    $progress['percentage'] = ($progress['processed'] / $progress['total']) * 100;
    $progress['last_update'] = time();
    
    // 상태 업데이트
    $state->set($progress_key, $progress);
    
    // 모든 아이템이 처리되었는지 확인
    if ($progress['processed'] >= $progress['total']) {
      // 완료 메시지 설정
      $state->set('mymodule.import_complete', TRUE);
      $state->set('mymodule.import_summary', [
        'total' => $progress['total'],
        'success' => $progress['success'],
        'failed' => $progress['failed'],
        'completed_at' => time(),
      ]);
      
      // 관리자에게 알림
      $this->notifyAdmin($progress);
    }
  }
  
  /**
   * 관리자에게 완료 알림 보내기.
   */
  protected function notifyAdmin($progress) {
    // 이메일 발송 등의 알림 로직
  }
}

그리고 이 진행 상황을 보여주는 컨트롤러:

/**
 * 진행 상황을 보여주는 컨트롤러.
 */
public function showProgress() {
  $state = \Drupal::state();
  $progress = $state->get('mymodule.import_progress', []);
  
  if (empty($progress)) {
    return [
      '#markup' => $this->t('현재 진행 중인 작업이 없습니다.'),
    ];
  }
  
  // 완료 여부 확인
  $complete = $state->get('mymodule.import_complete', FALSE);
  
  if ($complete) {
    $summary = $state->get('mymodule.import_summary', []);
    return [
      '#markup' => $this->t(
        '작업이 완료되었습니다. 총 @total개 중 @success개 성공, @failed개 실패.',
        [
          '@total' => $summary['total'],
          '@success' => $summary['success'],
          '@failed' => $summary['failed'],
        ]
      ),
    ];
  }
  
  // 진행 중인 경우 프로그레스 바 표시
  return [
    '#theme' => 'progress_bar',
    '#percent' => round($progress['percentage']),
    '#message' => $this->t(
      '@processed/@total 처리됨 (@percent%)',
      [
        '@processed' => $progress['processed'],
        '@total' => $progress['total'],
        '@percent' => round($progress['percentage']),
      ]
    ),
    '#attached' => [
      'library' => ['core/drupal.progress'],
      // 자동 새로고침을 위한 JavaScript
      'drupalSettings' => [
        'mymodule' => [
          'refreshInterval' => 5000, // 5초마다 새로고침
        ],
      ],
    ],
  ];
}

이렇게 하면 사용자는 작업의 진행 상황을 실시간으로 확인할 수 있어. 특히 오래 걸리는 데이터 마이그레이션이나 대량 처리 작업에서 중요한 기능이지! 📈

🔧 실전 예제: 대용량 이미지 처리 시스템

이제 실제 프로젝트에서 활용할 수 있는 완전한 예제를 살펴볼게. 이 예제는 대용량 이미지를 처리하는 시스템을 구현해. 사용자가 여러 이미지를 업로드하면 백그라운드에서 이미지를 처리하고 결과를 알려주는 시스템이야. 🖼️

이런 시스템은 재능넷과 같은 플랫폼에서 사용자들이 포트폴리오 이미지를 업로드할 때 유용하게 사용될 수 있어!

1️⃣ 모듈 구조 설정

먼저 모듈 구조를 설정해보자:

image_processor/
  ├── image_processor.info.yml
  ├── image_processor.module
  ├── image_processor.routing.yml
  ├── image_processor.services.yml
  ├── src/
  │   ├── Controller/
  │   │   └── ImageProcessorController.php
  │   ├── Form/
  │   │   └── ImageUploadForm.php
  │   ├── Plugin/
  │   │   └── QueueWorker/
  │   │       └── ImageProcessorWorker.php
  │   └── Service/
  │       └── ImageProcessorService.php
  └── templates/
      └── image-processor-status.html.twig

2️⃣ 서비스 정의

image_processor.services.yml 파일에 필요한 서비스를 정의해:

services:
  image_processor.queue:
    class: Drupal\Core\Queue\QueueInterface
    factory: ['@queue', 'get']
    arguments: ['image_processor']
    
  image_processor.service:
    class: Drupal\image_processor\Service\ImageProcessorService
    arguments: ['@image_processor.queue', '@entity_type.manager', '@logger.factory', '@state']

3️⃣ 이미지 업로드 폼 구현

사용자가 이미지를 업로드할 수 있는 폼을 만들어:

namespace Drupal\image_processor\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\image_processor\Service\ImageProcessorService;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * 이미지 업로드 폼.
 */
class ImageUploadForm extends FormBase {

  /**
   * 이미지 처리 서비스.
   *
   * @var \Drupal\image_processor\Service\ImageProcessorService
   */
  protected $imageProcessor;

  /**
   * 파일 시스템 서비스.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('image_processor.service'),
      $container->get('file_system')
    );
  }

  /**
   * 생성자.
   */
  public function __construct(ImageProcessorService $image_processor, FileSystemInterface $file_system) {
    $this->imageProcessor = $image_processor;
    $this->fileSystem = $file_system;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'image_processor_upload_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['description'] = [
      '#markup' => '<p>' . $this->t('이 폼을 사용하여 처리할 이미지를 업로드하세요. 이미지는 백그라운드에서 처리됩니다.') . '</p>',
    ];

    $form['images'] = [
      '#type' => 'managed_file',
      '#title' => $this->t('이미지 업로드'),
      '#description' => $this->t('JPG, PNG, GIF 파일을 업로드하세요. 여러 파일을 선택할 수 있습니다.'),
      '#upload_location' => 'public://image_processor/uploads',
      '#upload_validators' => [
        'file_validate_extensions' => ['jpg jpeg png gif'],
        'file_validate_size' => [10 * 1024 * 1024], // 10MB 제한
      ],
      '#multiple' => TRUE,
      '#required' => TRUE,
    ];

    $form['process_type'] = [
      '#type' => 'select',
      '#title' => $this->t('처리 유형'),
      '#options' => [
        'resize' => $this->t('크기 조정'),
        'watermark' => $this->t('워터마크 추가'),
        'filter' => $this->t('필터 적용'),
      ],
      '#default_value' => 'resize',
      '#required' => TRUE,
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('이미지 처리 시작'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $file_ids = $form_state->getValue('images');
    $process_type = $form_state->getValue('process_type');
    
    if (!empty($file_ids)) {
      // 배치 ID 생성
      $batch_id = uniqid('img_batch_');
      
      // 각 파일을 큐에 추가
      $count = $this->imageProcessor->queueImagesForProcessing($file_ids, $process_type, $batch_id);
      
      // 상태 페이지로 리다이렉트
      $form_state->setRedirect('image_processor.status', ['batch_id' => $batch_id]);
      
      $this->messenger()->addStatus($this->t('@count개의 이미지가 처리 대기열에 추가되었습니다. 처리 상태를 확인하세요.', ['@count' => $count]));
    }
  }
}

4️⃣ 이미지 처리 서비스 구현

이미지 처리 로직을 담당하는 서비스를 구현해:

namespace Drupal\image_processor\Service;

use Drupal\Core\Queue\QueueInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\State\StateInterface;

/**
 * 이미지 처리 서비스.
 */
class ImageProcessorService {

  /**
   * 이미지 처리 큐.
   *
   * @var \Drupal\Core\Queue\QueueInterface
   */
  protected $queue;

  /**
   * 엔티티 타입 매니저.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * 로거.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * 상태 API.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * 생성자.
   */
  public function __construct(
    QueueInterface $queue,
    EntityTypeManagerInterface $entity_type_manager,
    LoggerChannelFactoryInterface $logger_factory,
    StateInterface $state
  ) {
    $this->queue = $queue;
    $this->entityTypeManager = $entity_type_manager;
    $this->logger = $logger_factory->get('image_processor');
    $this->state = $state;
  }

  /**
   * 이미지를 처리 큐에 추가.
   *
   * @param array $file_ids
   *   파일 ID 배열.
   * @param string $process_type
   *   처리 유형.
   * @param string $batch_id
   *   배치 ID.
   *
   * @return int
   *   큐에 추가된 이미지 수.
   */
  public function queueImagesForProcessing(array $file_ids, $process_type, $batch_id) {
    $count = 0;
    $file_storage = $this->entityTypeManager->getStorage('file');
    
    // 배치 정보 초기화
    $this->initBatchStatus($batch_id, count($file_ids));
    
    foreach ($file_ids as $file_id) {
      $file = $file_storage->load($file_id);
      
      if ($file) {
        // 큐에 작업 추가
        $this->queue->createItem([
          'file_id' => $file_id,
          'process_type' => $process_type,
          'batch_id' => $batch_id,
          'created' => time(),
        ]);
        
        $count++;
        $this->logger->notice('이미지 @file_id가 처리 큐에 추가됨 (배치: @batch_id)', [
          '@file_id' => $file_id,
          '@batch_id' => $batch_id,
        ]);
      }
    }
    
    return $count;
  }

  /**
   * 배치 상태 초기화.
   *
   * @param string $batch_id
   *   배치 ID.
   * @param int $total
   *   총 이미지 수.
   */
  protected function initBatchStatus($batch_id, $total) {
    $status_key = 'image_processor.batch.' . $batch_id;
    
    $this->state->set($status_key, [
      'id' => $batch_id,
      'total' => $total,
      'processed' => 0,
      'success' => 0,
      'failed' => 0,
      'created' => time(),
      'updated' => time(),
      'completed' => FALSE,
    ]);
  }

  /**
   * 배치 상태 업데이트.
   *
   * @param string $batch_id
   *   배치 ID.
   * @param bool $success
   *   처리 성공 여부.
   */
  public function updateBatchStatus($batch_id, $success = TRUE) {
    $status_key = 'image_processor.batch.' . $batch_id;
    $status = $this->state->get($status_key);
    
    if ($status) {
      $status['processed']++;
      if ($success) {
        $status['success']++;
      }
      else {
        $status['failed']++;
      }
      
      $status['updated'] = time();
      
      // 모든 이미지가 처리되었는지 확인
      if ($status['processed'] >= $status['total']) {
        $status['completed'] = TRUE;
      }
      
      $this->state->set($status_key, $status);
    }
  }

  /**
   * 배치 상태 가져오기.
   *
   * @param string $batch_id
   *   배치 ID.
   *
   * @return array|null
   *   배치 상태 정보.
   */
  public function getBatchStatus($batch_id) {
    $status_key = 'image_processor.batch.' . $batch_id;
    return $this->state->get($status_key);
  }

  /**
   * 이미지 처리 로직.
   *
   * @param int $file_id
   *   파일 ID.
   * @param string $process_type
   *   처리 유형.
   *
   * @return bool
   *   처리 성공 여부.
   */
  public function processImage($file_id, $process_type) {
    try {
      $file_storage = $this->entityTypeManager->getStorage('file');
      $file = $file_storage->load($file_id);
      
      if (!$file) {
        $this->logger->error('파일을 찾을 수 없음: @file_id', ['@file_id' => $file_id]);
        return FALSE;
      }
      
      // 파일 URI 가져오기
      $uri = $file->getFileUri();
      
      // 처리 유형에 따라 다른 처리 수행
      switch ($process_type) {
        case 'resize':
          $success = $this->resizeImage($uri);
          break;
          
        case 'watermark':
          $success = $this->addWatermark($uri);
          break;
          
        case 'filter':
          $success = $this->applyFilter($uri);
          break;
          
        default:
          $this->logger->error('알 수 없는 처리 유형: @type', ['@type' => $process_type]);
          return FALSE;
      }
      
      if ($success) {
        $this->logger->notice('이미지 처리 성공: @file_id (@type)', [
          '@file_id' => $file_id,
          '@type' => $process_type,
        ]);
      }
      
      return $success;
    }
    catch (\Exception $e) {
      $this->logger->error('이미지 처리 중 오류 발생: @error', ['@error' => $e->getMessage()]);
      return FALSE;
    }
  }

  /**
   * 이미지 크기 조정.
   */
  protected function resizeImage($uri) {
    // 실제 이미지 처리 로직 (Image API 사용)
    // 예시 코드이므로 실제 구현은 생략
    $this->logger->notice('이미지 크기 조정: @uri', ['@uri' => $uri]);
    
    // 성공 시뮬레이션 (90% 확률로 성공)
    return rand(1, 10) <= 9;
  }

  /**
   * 워터마크 추가.
   */
  protected function addWatermark($uri) {
    // 워터마크 추가 로직
    $this->logger->notice('워터마크 추가: @uri', ['@uri' => $uri]);
    
    // 성공 시뮬레이션
    return rand(1, 10) <= 9;
  }

  /**
   * 필터 적용.
   */
  protected function applyFilter($uri) {
    // 필터 적용 로직
    $this->logger->notice('필터 적용: @uri', ['@uri' => $uri]);
    
    // 성공 시뮬레이션
    return rand(1, 10) <= 9;
  }
}

5️⃣ 큐 워커 구현

이미지 처리를 담당할 QueueWorker 플러그인을 구현해:

namespace Drupal\image_processor\Plugin\QueueWorker;

use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\image_processor\Service\ImageProcessorService;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * 이미지 처리 큐 워커.
 *
 * @QueueWorker(
 *   id = "image_processor",
 *   title = @Translation("Image Processor Worker"),
 *   cron = {"time" = 60}
 * )
 */
class ImageProcessorWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {

  /**
   * 이미지 처리 서비스.
   *
   * @var \Drupal\image_processor\Service\ImageProcessorService
   */
  protected $imageProcessor;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('image_processor.service')
    );
  }

  /**
   * 생성자.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, ImageProcessorService $image_processor) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->imageProcessor = $image_processor;
  }

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    // 데이터에서 필요한 정보 추출
    $file_id = $data['file_id'];
    $process_type = $data['process_type'];
    $batch_id = $data['batch_id'];
    
    // 이미지 처리
    $success = $this->imageProcessor->processImage($file_id, $process_type);
    
    // 배치 상태 업데이트
    $this->imageProcessor->updateBatchStatus($batch_id, $success);
  }
}

6️⃣ 상태 확인 컨트롤러

작업 진행 상황을 확인할 수 있는 컨트롤러를 구현해:

namespace Drupal\image_processor\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\image_processor\Service\ImageProcessorService;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * 이미지 처리 컨트롤러.
 */
class ImageProcessorController extends ControllerBase {

  /**
   * 이미지 처리 서비스.
   *
   * @var \Drupal\image_processor\Service\ImageProcessorService
   */
  protected $imageProcessor;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('image_processor.service')
    );
  }

  /**
   * 생성자.
   */
  public function __construct(ImageProcessorService $image_processor) {
    $this->imageProcessor = $image_processor;
  }

  /**
   * 배치 상태 페이지.
   */
  public function statusPage($batch_id, Request $request) {
    $status = $this->imageProcessor->getBatchStatus($batch_id);
    
    if (!$status) {
      return [
        '#markup' => $this->t('배치 ID @batch_id에 대한 정보를 찾을 수 없습니다.', ['@batch_id' => $batch_id]),
      ];
    }
    
    // 진행률 계산
    $percent = ($status['total'] > 0) ? round(($status['processed'] / $status['total']) * 100) : 0;
    
    // 템플릿 변수 설정
    $build = [
      '#theme' => 'image_processor_status',
      '#status' => $status,
      '#percent' => $percent,
      '#attached' => [
        'library' => ['core/drupal.progress'],
      ],
    ];
    
    // 작업이 완료되지 않았으면 자동 새로고침 설정
    if (!$status['completed']) {
      $build['#attached']['html_head'][] = [
        [
          '#tag' => 'meta',
          '#attributes' => [
            'http-equiv' => 'refresh',
            'content' => '5', // 5초마다 새로고침
          ],
        ],
        'image_processor_refresh',
      ];
    }
    
    return $build;
  }
}

7️⃣ 템플릿 구현

상태 페이지의 템플릿을 구현해:

{% set status_class = status.completed ? 'completed' : 'in-progress' %}

<div class="image-processor-status {{ status_class }}">
  <h2>{{ 'Image Processing Status'|t }}</h2>
  
  <div class="batch-info">
    <p><strong>{{ 'Batch ID'|t }}:</strong> {{ status.id }}</p>
    <p><strong>{{ 'Started'|t }}:</strong> {{ status.created|date('Y-m-d H:i:s') }}</p>
    <p><strong>{{ 'Last updated'|t }}:</strong> {{ status.updated|date('Y-m-d H:i:s') }}</p>
  </div>
  
  <div class="progress-info">
    <div class="progress" data-drupal-progress>
      <div class="progress__track">
        <div class="progress__bar" style="width: {{ percent }}%"></div>
      </div>
      <div class="progress__percentage">{{ percent }}%</div>
    </div>
    
    <div class="progress-stats">
      <p>{{ 'Processed'|t }}: {{ status.processed }} / {{ status.total }}</p>
      <p>{{ 'Success'|t }}: {{ status.success }}</p>
      <p>{{ 'Failed'|t }}: {{ status.failed }}</p>
    </div>
  </div>
  
  {% if status.completed %}
    <div class="completion-message">
      <p>{{ 'Processing completed!'|t }}</p>
      <p>{{ 'Total time'|t }}: {{ ((status.updated - status.created) / 60)|round(1) }} {{ 'minutes'|t }}</p>
    </div>
  {% else %}
    <div class="in-progress-message">
      <p>{{ 'Processing in progress. This page will refresh automatically every 5 seconds.'|t }}</p>
    </div>
  {% endif %}
  
  <div class="actions">
    <a href="%7B%7B%20path('image_processor.upload')%20%7D%7D" class="button">{{ 'Upload More Images'|t }}</a>
  </div>
</div>

이 예제는 완전한 이미지 처리 시스템을 구현한 거야. 사용자가 이미지를 업로드하면 백그라운드에서 처리되고, 사용자는 실시간으로 진행 상황을 확인할 수 있어. 이런 패턴은 재능넷과 같은 플랫폼에서 사용자 콘텐츠를 효율적으로 처리하는 데 매우 유용해! 🚀

🔥 성능 최적화 팁

Queue API를 사용할 때 성능을 최적화하기 위한 몇 가지 팁을 알아볼게:

Queue API 성능 최적화 전략 배치 처리 대량의 작은 작업 대신 적은 수의 큰 배치로 처리 데이터베이스 부하 감소 오버헤드 최소화 메모리 관리 작업 처리 후 메모리 해제 큰 객체는 참조 해제 가비지 컬렉션 고려 메모리 누수 방지 최적의 백엔드 선택 Database: 간단한 작업 Redis: 고성능 필요 시 Beanstalkd: 복잡한 작업 관리 작업 특성에 맞는 백엔드 선택 전용 워커 프로세스 Cron 대신 전용 워커 사용 Drush 명령어로 워커 실행 Supervisor로 프로세스 관리 병렬 처리로 성능 향상

1️⃣ 배치 처리 최적화

작은 작업을 많이 만드는 것보다 적절한 크기의 배치로 묶어서 처리하는 것이 효율적이야. 이렇게 하면 큐 관리 오버헤드를 줄일 수 있어:

// 비효율적인 방법: 각 항목마다 큐 아이템 생성
foreach ($items as $item) {
  $queue->createItem($item);
}

// 효율적인 방법: 배치로 묶어서 처리
$batches = array_chunk($items, 100);
foreach ($batches as $batch) {
  $queue->createItem(['items' => $batch]);
}

2️⃣ 메모리 관리

큐 작업자가 오랫동안 실행될 때는 메모리 관리가 중요해. 특히 대용량 데이터를 처리할 때 메모리 누수를 방지해야 해:

/**
 * 메모리 관리를 고려한 큐 워커.
 */
public function processItem($data) {
  // 대용량 데이터 처리
  $result = $this->processLargeData($data);
  
  // 처리 완료 후 대용량 변수 명시적으로 해제
  unset($data);
  
  // 가비지 컬렉션 강제 실행 (필요한 경우에만)
  if (gc_enabled()) {
    gc_collect_cycles();
  }
  
  return $result;
}

3️⃣ 최적의 큐 백엔드 선택

작업의 특성에 따라 적절한 큐 백엔드를 선택하는 것이 중요해:

  1. Database Queue - 간단한 작업, 추가 의존성 없음, 기본 설정으로 사용 가능
  2. Redis Queue - 고성능 필요, 많은 양의 큐 아이템, 빠른 처리 속도 요구
  3. Beanstalkd Queue - 복잡한 작업 관리, 우선순위 지원, 지연 처리 필요

대규모 사이트에서는 Redis나 Beanstalkd와 같은 전용 큐 서비스를 사용하는 것이 좋아. 이들은 데이터베이스 큐보다 훨씬 빠르고 확장성이 뛰어나! 🚀

4️⃣ 전용 워커 프로세스 사용

cron에 의존하는 대신 전용 워커 프로세스를 사용하면 큐 처리 성능을 크게 향상시킬 수 있어:

// drush 명령어로 큐 워커 실행
drush queue:run image_processor

이 명령어를 Supervisor와 같은 프로세스 관리자로 관리하면 지속적으로 실행되는 워커를 만들 수 있어:

# /etc/supervisor/conf.d/drupal-queue-workers.conf
[program:drupal-image-processor]
command=/usr/local/bin/drush --root=/var/www/drupal queue:run image_processor
directory=/var/www/drupal
autostart=true
autorestart=true
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
stderr_logfile=/var/log/supervisor/drupal-queue-error.log
stdout_logfile=/var/log/supervisor/drupal-queue.log

이렇게 설정하면 2개의 워커 프로세스가 병렬로 실행되어 큐 처리 성능이 크게 향상돼! 🔄

5️⃣ 오류 처리 및 모니터링

큐 작업의 오류를 효과적으로 처리하고 모니터링하는 것도 중요해:

  1. 로깅 - 모든 큐 작업의 시작, 완료, 오류를 로깅
  2. 알림 - 심각한 오류 발생 시 관리자에게 알림
  3. 재시도 정책 - 일시적인 오류에 대한 재시도 정책 설정
  4. 데드 레터 큐 - 여러 번 실패한 작업을 별도의 큐로 이동

이런 모니터링 시스템을 구축하면 큐 작업의 안정성을 크게 향상시킬 수 있어! 📊

🔮 드루팔 10 이후의 Queue API 발전 방향

드루팔 10에서 Queue API가 크게 개선되었지만, 앞으로 더 발전할 가능성이 있어. 현재 논의되고 있는 몇 가지 발전 방향을 살펴볼게:

1️⃣ 비동기 이벤트 시스템과의 통합

드루팔의 이벤트 시스템과 Queue API를 더 긴밀하게 통합하는 방향으로 발전하고 있어. 이를 통해 이벤트 기반 아키텍처를 더 쉽게 구현할 수 있을 거야:

// 이벤트 구독자에서 비동기 처리
public function onUserRegistered(UserRegisteredEvent $event) {
  // 이벤트를 큐에 추가하여 비동기적으로 처리
  \Drupal::queue('user_welcome_email')->createItem([
    'user_id' => $event->getUser()->id(),
    'event_time' => time(),
  ]);
}

2️⃣ 메시지 브로커 통합

RabbitMQ, Apache Kafka와 같은 고급 메시지 브로커와의 통합이 개발 중이야. 이를 통해 더 복잡한 메시징 패턴을 지원할 수 있을 거야:

  • 발행-구독 패턴
  • 메시지 라우팅
  • 우선순위 큐
  • 지연 메시지
  • 분산 처리

3️⃣ 실시간 모니터링 및 관리 도구

큐 작업을 실시간으로 모니터링하고 관리할 수 있는 도구가 개발 중이야. 이를 통해 관리자는 큐 상태를 쉽게 확인하고 문제를 해결할 수 있을 거야:

  1. 대시보드에서 큐 상태 실시간 확인
  2. 작업 우선순위 조정
  3. 실패한 작업 재시도
  4. 큐 성능 분석

4️⃣ 스케줄링 기능 강화

현재 Queue API는 기본적인 지연 실행만 지원하지만, 앞으로는 더 강력한 스케줄링 기능이 추가될 예정이야:

// 미래의 API (예시)
$queue_factory->get('scheduled_tasks')
  ->schedule([
    'action' => 'send_newsletter',
    'data' => ['newsletter_id' => 123],
  ])
  ->at(strtotime('next Monday 9:00 AM'))
  ->withRetry(['max_attempts' => 3, 'backoff' => 'exponential'])
  ->create();

5️⃣ 서버리스 아키텍처 지원

AWS Lambda, Google Cloud Functions와 같은 서버리스 환경에서 큐 작업을 처리할 수 있는 통합 기능이 개발 중이야. 이를 통해 확장성과 비용 효율성을 크게 향상시킬 수 있을 거야:

// 서버리스 함수로 큐 작업 처리 (개념적 예시)
$queue_factory->get('image_processing')
  ->setProcessor('serverless')
  ->setConfig([
    'provider' => 'aws_lambda',
    'function' => 'drupal-image-processor',
    'region' => 'us-east-1',
  ]);

이러한 발전은 드루팔이 엔터프라이즈급 애플리케이션 플랫폼으로 더욱 성장하는 데 기여할 거야. 특히 재능넷과 같은 대규모 플랫폼에서는 이런 고급 기능들이 매우 유용할 거야! 🚀

📝 결론: Queue API로 드루팔 사이트 성능 극대화하기

지금까지 드루팔의 Queue API에 대해 자세히 알아봤어. 이제 정리해볼게! 🎯

Queue API는 대규모 배치 작업을 효율적으로 처리하기 위한 드루팔의 강력한 도구야. 이를 통해 다음과 같은 이점을 얻을 수 있어:

  1. 사용자 경험 향상 - 무거운 작업을 백그라운드로 옮겨 사용자 응답 시간 개선
  2. 서버 리소스 최적화 - 작업을 분산하여 서버 부하 관리
  3. 안정성 향상 - 실패한 작업의 재시도 메커니즘 제공
  4. 확장성 확보 - 대량의 작업을 효율적으로 처리할 수 있는 구조
  5. 모듈화된 코드 - 작업 처리 로직을 명확하게 분리

특히 재능넷과 같은 사용자 참여형 플랫폼에서는 다음과 같은 작업에 Queue API를 활용할 수 있어:

  1. 대량의 사용자 알림 발송
  2. 사용자 업로드 콘텐츠 처리 (이미지 리사이징, 워터마크 등)
  3. 정기적인 데이터 분석 및 보고서 생성
  4. 외부 API와의 통합 (결제, 소셜 미디어 등)
  5. 대용량 데이터 임포트/엑스포트

드루팔 10에서는 Queue API가 더욱 강화되어 엔터프라이즈급 애플리케이션을 구축하는 데 필수적인 도구가 되었어. 앞으로도 계속해서 발전할 이 기능을 잘 활용하면 드루팔 사이트의 성능과 사용자 경험을 크게 향상시킬 수 있을 거야! 💪

이 글이 드루팔 Queue API를 이해하고 활용하는 데 도움이 되었길 바라. 더 많은 웹 개발 관련 정보는 재능넷에서 확인할 수 있어. 다양한 개발자들이 자신의 지식과 경험을 공유하는 플랫폼이니 한번 방문해보는 것도 좋을 것 같아! 🌟

질문이나 의견이 있다면 언제든지 댓글로 남겨줘. 함께 배우고 성장하는 여정을 계속해보자! 👋

🌟 드루팔 Queue API란 뭐야? 친구처럼 쉽게 설명해줄게!

안녕! 오늘은 드루팔의 숨은 영웅인 Queue API에 대해 함께 알아볼 거야. 웹사이트를 운영하다 보면 대용량 데이터를 처리하거나 시간이 오래 걸리는 작업을 해야 할 때가 있잖아? 이럴 때 사용자가 "로딩 중..." 화면을 보며 지루하게 기다리게 하는 건 최악의 경험이지! 😱

바로 이런 상황에서 Queue API가 우리의 구원자로 등장해. 간단히 말하면, Queue API는 시간이 오래 걸리는 작업들을 "대기열"에 넣어두고 백그라운드에서 차근차근 처리해주는 드루팔의 강력한 기능이야. 마치 카페에서 주문을 받고 번호표를 나눠주는 것처럼, 작업을 접수받고 순서대로 처리하는 시스템이지! ☕

드루팔 Queue API 작동 방식 사용자 드루팔 웹사이트 Queue API Worker 백그라운드 처리 1. 작업 요청 2. 큐에 작업 추가 3. 백그라운드 처리

웹 개발자라면 누구나 공감할 거야. 이미지 수천 장을 처리하거나, 대량의 데이터를 마이그레이션하거나, 복잡한 보고서를 생성할 때 사용자가 브라우저에서 "처리 중..." 화면을 보며 기다리게 하는 건 최악의 UX지. Queue API를 사용하면 이런 작업들을 백그라운드로 옮겨서 사용자는 다른 일을 할 수 있게 해주고, 작업이 완료되면 알려줄 수 있어. 👍

🔍 왜 Queue API가 필요한 걸까?

웹사이트를 운영하다 보면 다음과 같은 상황에 자주 부딪히게 돼:

  1. 시간이 오래 걸리는 작업 - 대용량 파일 처리, 데이터 마이그레이션 등
  2. 주기적으로 실행해야 하는 작업 - 데이터 정리, 백업, 보고서 생성
  3. 외부 API와의 통신 - 응답이 느린 외부 서비스와 통신할 때
  4. 리소스를 많이 사용하는 작업 - 서버에 부하를 줄 수 있는 무거운 작업
  5. 실패 시 재시도가 필요한 작업 - 네트워크 오류 등으로 실패할 수 있는 작업

이런 작업들을 그냥 웹 요청으로 처리하면 어떤 문제가 생길까? 🤔

🚫 Queue 없이 처리할 때의 문제점

1. 타임아웃 발생 - PHP의 기본 실행 시간 제한(보통 30초)을 초과하면 작업이 중단돼.

2. 메모리 부족 - 대용량 데이터 처리 시 메모리 한계에 도달할 수 있어.

3. 사용자 경험 저하 - 사용자가 작업이 완료될 때까지 기다려야 해.

4. 서버 부하 증가 - 동시에 여러 무거운 작업이 실행되면 서버가 과부하될 수 있어.

5. 작업 추적 어려움 - 작업 상태나 진행 상황을 추적하기 어려워.

이런 문제들을 해결하기 위해 드루팔은 Queue API를 제공해. 이 API를 사용하면 작업을 대기열에 넣고, 백그라운드에서 처리하고, 실패 시 재시도하고, 작업 상태를 추적할 수 있어. 특히 재능넷과 같은 사용자 참여형 플랫폼에서는 대량의 데이터 처리나 알림 발송 같은 작업을 Queue API로 처리하면 사이트 성능을 크게 향상시킬 수 있어! 💪

🛠️ 드루팔 10의 Queue API 구조

드루팔 8부터 도입되고 드루팔 10에서 더욱 강화된 Queue API는 다음과 같은 주요 컴포넌트로 구성돼 있어:

Queue API 구조 QueueInterface QueueFactory QueueWorker DatabaseQueue MemoryQueue RedisQueue BeanstalkdQueue QueueItem

이 구조에서 각 컴포넌트의 역할을 살펴볼게:

  1. QueueInterface - 모든 큐 구현체가 따라야 하는 인터페이스야. 작업을 추가하고, 가져오고, 삭제하는 메서드를 정의해.
  2. QueueFactory - 다양한 큐 구현체를 생성하는 팩토리 클래스야. 설정에 따라 적절한 큐 구현체를 제공해.
  3. QueueWorker - 큐에서 작업을 가져와 실행하는 워커야. 작업 실행, 실패 처리, 재시도 등을 담당해.
  4. Queue 구현체 - 실제 큐 기능을 구현한 클래스들이야:
    1. DatabaseQueue - 드루팔 데이터베이스를 사용하는 기본 구현체
    2. MemoryQueue - 메모리에서 작동하는 경량 구현체 (주로 테스트용)
    3. RedisQueue - Redis를 백엔드로 사용하는 구현체
    4. BeanstalkdQueue - Beanstalkd 큐 서버를 사용하는 구현체
  5. QueueItem - 큐에 저장되는 작업 아이템이야. 실행할 콜백, 데이터, 태그 등을 포함해.

드루팔 10에서는 이 구조가 더욱 모듈화되고 확장 가능하게 개선되었어. 특히 다양한 백엔드를 쉽게 추가할 수 있는 플러그인 시스템이 강화되었지! 🔌

🚀 Queue API 시작하기: 기본 설정

자, 이제 실제로 Queue API를 사용해보자! 먼저 기본 설정부터 시작할게.

1️⃣ 필요한 모듈 설치하기

드루팔 10에서는 Queue API가 코어에 포함되어 있지만, 실제로 사용하려면 Queue 모듈을 설치해야 해:

composer require drupal/core-queue

추가적인 기능이 필요하다면 다음 모듈들도 고려해볼 수 있어:

  1. Advanced Queue - 더 많은 기능을 제공하는 확장 모듈
    composer require drupal/advancedqueue
  2. Queue UI - 큐 관리를 위한 UI를 제공하는 모듈
    composer require drupal/queue_ui
  3. Redis - Redis 백엔드를 사용하기 위한 모듈
    composer require drupal/redis

2️⃣ 큐 설정하기

기본적으로 드루팔은 데이터베이스를 큐 백엔드로 사용해. 하지만 settings.php 파일에서 다른 백엔드를 설정할 수 있어:

// Database 큐 사용 (기본값)
$settings['queue_default'] = 'database';

// Redis 큐 사용
$settings['queue_default'] = 'redis';
$settings['redis.connection']['host'] = '127.0.0.1';
$settings['redis.connection']['port'] = 6379;

// 특정 큐에 대해 다른 백엔드 사용
$settings['queue_service_myqueue'] = 'redis';

여기서 'myqueue'는 네가 정의할 큐의 이름이야. 여러 큐를 만들어서 각각 다른 용도로 사용할 수 있어! 🎯

3️⃣ Cron 설정하기

큐 작업을 주기적으로 처리하려면 Cron을 설정해야 해. 드루팔의 cron.php를 주기적으로 실행하도록 서버의 crontab을 설정하는 게 좋아:

*/15 * * * * wget -O - -q -t 1 http://example.com/cron.php?cron_key=YOUR_CRON_KEY

또는 Drush를 사용할 수도 있어:

*/15 * * * * cd /path/to/drupal && drush core:cron

큐 작업이 많거나 자주 실행해야 한다면 별도의 워커 프로세스를 설정하는 것이 좋아. 이건 나중에 더 자세히 설명할게! ⏱️

💡 Queue API 기본 사용법

이제 Queue API의 기본적인 사용법을 알아볼게. 크게 세 가지 단계로 나눌 수 있어:

  1. 큐 정의하기
  2. 큐에 작업 추가하기
  3. 큐 작업 처리하기

1️⃣ 큐 정의하기

먼저 모듈의 mymodule.services.yml 파일에 큐 서비스를 정의해:

services:
  mymodule.my_queue:
    class: Drupal\Core\Queue\QueueInterface
    factory: ['@queue', 'get']
    arguments: ['mymodule_queue_name']

이렇게 하면 'mymodule_queue_name'이라는 이름의 큐를 서비스로 등록하게 돼. 이제 이 서비스를 의존성 주입으로 가져와서 사용할 수 있어! 🧩

2️⃣ 큐에 작업 추가하기

이제 큐에 작업을 추가해보자. 다음은 컨트롤러나 서비스에서 큐에 작업을 추가하는 예시야:

/**
 * @param \Drupal\Core\Queue\QueueFactory $queue_factory
 */
public function addItemsToQueue(QueueFactory $queue_factory) {
  // 큐 가져오기
  $queue = $queue_factory->get('mymodule_queue_name');
  
  // 데이터 준비
  $data = [
    'user_id' => 123,
    'operation' => 'send_email',
    'params' => ['subject' => '안녕하세요!', 'body' => '큐 API 테스트입니다.'],
  ];
  
  // 큐에 아이템 추가
  $queue->createItem($data);
  
  return ['#markup' => '큐에 작업이 추가되었습니다!'];
}

의존성 주입을 사용하지 않는다면 다음과 같이 할 수도 있어:

// 서비스 컨테이너에서 큐 팩토리 가져오기
$queue_factory = \Drupal::service('queue');

// 큐 가져오기
$queue = $queue_factory->get('mymodule_queue_name');

// 큐에 아이템 추가
$queue->createItem($data);

$data에는 어떤 PHP 데이터든 넣을 수 있어. 단, 직렬화가 가능해야 하니 객체를 넣을 때는 주의해야 해! 🧐

3️⃣ 큐 작업 처리하기

큐에 추가한 작업을 처리하는 방법은 두 가지가 있어:

A. QueueWorker 플러그인 사용하기

가장 권장되는 방법은 QueueWorker 플러그인을 만드는 거야. 이 방법을 사용하면 드루팔이 cron 실행 시 자동으로 큐 작업을 처리해줘.

먼저 src/Plugin/QueueWorker 디렉토리에 플러그인 클래스를 만들어:

namespace Drupal\mymodule\Plugin\QueueWorker;

use Drupal\Core\Queue\QueueWorkerBase;

/**
 * 이메일 발송 큐 작업자.
 *
 * @QueueWorker(
 *   id = "mymodule_queue_name",
 *   title = @Translation("My Module Queue Worker"),
 *   cron = {"time" = 60}
 * )
 */
class MyQueueWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    // 큐 아이템 처리 로직
    \Drupal::logger('mymodule')->notice('큐 아이템 처리 중: @data', ['@data' => print_r($data, TRUE)]);
    
    // 예: 이메일 발송
    if ($data['operation'] === 'send_email') {
      $mailManager = \Drupal::service('plugin.manager.mail');
      $mailManager->mail(
        'mymodule',
        'notice',
        'user@example.com',
        'ko',
        [
          'subject' => $data['params']['subject'],
          'body' => $data['params']['body'],
        ]
      );
    }
  }
}

이 플러그인의 @QueueWorker 어노테이션에서 중요한 부분은:

  1. id - 처리할 큐의 이름 (위에서 정의한 큐 이름과 일치해야 함)
  2. title - 관리 UI에 표시될 제목
  3. cron - cron 실행 시 처리할 시간(초) 제한

이렇게 설정하면 드루팔이 cron 실행 시 자동으로 큐 아이템을 처리해줘. 각 cron 실행마다 최대 60초 동안 큐 아이템을 처리하게 돼. 👍

B. 수동으로 큐 처리하기

때로는 cron을 기다리지 않고 즉시 큐를 처리하고 싶을 수도 있어. 이럴 때는 다음과 같이 수동으로 처리할 수 있어:

// 큐와 워커 가져오기
$queue = \Drupal::queue('mymodule_queue_name');
$queue_manager = \Drupal::service('plugin.manager.queue_worker');
$queue_worker = $queue_manager->createInstance('mymodule_queue_name');

// 큐에서 아이템을 가져와 처리
while ($item = $queue->claimItem()) {
  try {
    $queue_worker->processItem($item->data);
    $queue->deleteItem($item);
  }
  catch (\Exception $e) {
    // 오류 발생 시 아이템 릴리스 (다시 처리할 수 있도록)
    $queue->releaseItem($item);
    watchdog_exception('mymodule', $e);
  }
}

이 방법은 Drush 명령어나 특정 관리 페이지에서 큐를 즉시 처리하고 싶을 때 유용해! 🚀