라라벨 이벤트와 큐에 대한 세 번째 마지막 포스팅이다. 


첫 번째 포스팅에서는 라라벨에서 이벤트를 발생시키고 리스너에서 어떻게 그 이벤트를 처리하는지에 대해 설명하였다.  



두 번째 포스팅에서는 이벤트 즉, 태스크를 완료처리하는 시점을 이벤트로 정의하고 그 이벤트가 발생했을 때 메일을 발송하도록 하였다.



여기서 노출되는 문제는 처리시간의 지연이다. 즉, 이벤트를 완료처리하는데 있어 메일발송이 너무 많은 시간을 잡아먹는다는 것이다. 


그래서 이번 시간에는 이 메일발송을 라라벨이 지원하는 큐 방식으로 처리하도록 변경하려고 한다. 그러면 실제 메일 발송은 큐 워커(Queue worker)에 의해 백그라운드로 이루어지고 태스크에 대한 완료처리는 사용자에게 바로 피드백을 줄 수 있게 된다. 


라라벨 프레임워크에서 지원하는 큐에 대한 설정은 queue.php 파일에서 볼 수 있다. 


<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Queue Connection Name
    |--------------------------------------------------------------------------
    |
    | Laravel's queue API supports an assortment of back-ends via a single
    | API, giving you convenient access to each back-end using the same
    | syntax for every one. Here you may define a default connection.
    |
    */

    'default' => env('QUEUE_CONNECTION', 'sync'),

    /*
    |--------------------------------------------------------------------------
    | Queue Connections
    |--------------------------------------------------------------------------
    |
    | Here you may configure the connection information for each server that
    | is used by your application. A default configuration has been added
    | for each back-end shipped with Laravel. You are free to add more.
    |
    | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
    |
    */

    'connections' => [

        'sync' => [
            'driver' => 'sync',
        ],

        'database' => [
            'driver' => 'database',
            'table' => 'jobs',
            'queue' => 'default',
            'retry_after' => 90,
        ],

        'beanstalkd' => [
            'driver' => 'beanstalkd',
            'host' => 'localhost',
            'queue' => 'default',
            'retry_after' => 90,
        ],

        'sqs' => [
            'driver' => 'sqs',
            'key' => env('SQS_KEY', 'your-public-key'),
            'secret' => env('SQS_SECRET', 'your-secret-key'),
            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
            'queue' => env('SQS_QUEUE', 'your-queue-name'),
            'region' => env('SQS_REGION', 'us-east-1'),
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
            'queue' => env('REDIS_QUEUE', 'default'),
            'retry_after' => 90,
            'block_for' => null,
        ],

    ],

    /*
    |--------------------------------------------------------------------------
    | Failed Queue Jobs
    |--------------------------------------------------------------------------
    |
    | These options configure the behavior of failed queue job logging so you
    | can control which database and table are used to store the jobs that
    | have failed. You may change them to any database / table you wish.
    |
    */

    'failed' => [
        'database' => env('DB_CONNECTION', 'mysql'),
        'table' => 'failed_jobs',
    ],

];
?>

설정파일에서 보듯이 라라벨에서 지원하는 큐에는 Amazon SQS나 redis 등 유명한 몇 가지가 있다. 하지만 필자의 개발환경은 로컬 윈도우이기 때문에 여기서는 database 옵션을 사용하기로 한다. 


별다른 세팅을 하지 않을 경우 라라벨에서의 기본 설정은 sync인데 이것을 database로 바꾸려면 환경설정파일(.env)의 QUEUE_CONNECTION 항목에 다음과 같이 database 옵션을 지정해준다.


QUEUE_CONNECTION=database




1. database 큐(Queue) 세팅


데이터베이스를 큐로 사용하기 위해서는 큐와 관련된 정보를 저장할 테이블이 필요하다. 라라벨에서 php artisan 명령어를 통해 이 테이블을 생성할 수 있다. 


php artisan queue:table
php artisan queue:failed-table


만일 큐에 있는 Job을 처리하다 문제가 발생하면 재시도를 하게 되고 재시도 횟수를 초과하는 경우 실패한 Job에 대한 정보를 기록하게 되는데 이러한 데이터를 저장할 테이블도 필요하므로 같이 만들어준다.  


위의 실행결과로 몇개의 migration 파일이 생성되는데 migrate 명령어로 실제 테이블을 생성할 수 있다.


$ php artisan migrate
Migrating: 2018_12_27_223834_create_jobs_table
Migrated: 2018_12_27_223834_create_jobs_table
Migrating: 2018_12_28_151553_create_failed_jobs_table
Migrated: 2018_12_28_151553_create_failed_jobs_table


이제 라라벨에서 큐를 사용하려면 실제 로직을 처리할 Job을 만들고 이 Job을 미리 설정한 큐에 보내면 된다. Job을 큐로 보내려면 Job이 가진 dispatch() 메소드를 호출하면 된다. 이때 처리에 필요한 데이터를 같이 보내면 된다. 





2. Job 만들기


먼저 큐에 보낼 Job을 생성한다. 라라벨에서 Job 생성은 역시 php artisan 명령어를 사용하면 된다.


php artisan make:job SendEmailJob


그러면 아래와 같이 Job 클래스가 생성된다.

<?php

namespace App\Jobs;

use App\Models\Task;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Mail;

class SendEmailJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $task;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct(Task $task)
    {
        $this->task = $task;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Mail::to($this->task->user)
            ->send(new \App\Mail\TaskCompleted($this->task));
    }
}
?>

이 Job은 메일을 발송하는 역할을 한다. 여기에서도 Job이 로직을 처리하는데 필요하다면 어떠한 데이터라도 생성자를 통해 받아올 수 있는데 우리는 태스크에 대한 정보가 필요하므로 완료처리된 Task의 Eloquent 모델객체를 받아온다. (물론 데이터 객체는 이 Job을 큐에 보내는 쪽에서 호출할 때 같이 던져주어야 한다) 그리고 handle() 메소드에서 Mail 파사드를 이용해 메일을 발송하는 로직처리를 한다. 


여기서 생성된 코드를 보면 SendEmailJob이 ShouldQueue 인터페이스를 구현하고 있는데 이 인터페이스가 실제 Job을 처리할 때 비동기적으로 처리할 수 있도록 하는 역할을 한다. 




3. Queue worker로 Job 실행하기


이제 이렇게 생성한 Job이 처리되도록 하자. Job은 컨트롤러에서 호출될 수 도 있고 우리 경우에서처럼 이벤트가 발생할 때 호출될 수 도 있다. 우리는 TaskCompleted 이벤트가 발생되었을 때 이 Job이 실행되도록 할 것이다. 


그러므로 리스너로 돌아가 해당 코드를 변경하자.

<?php

namespace App\Listeners;

use App\Events\TaskCompleted;
use App\Jobs\SendEmailJob;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;

class SendEmailForCompletion
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  TaskCompleted  $event
     * @return void
     */
    public function handle(TaskCompleted $event)
    {
        Log::info(get_class($event) . ' 이벤트를 수신하였습니다.');
        Log::info(sprintf('%s (%s)님에게 완료알림 메일을 보내려고 합니다', $event->task->user->name, $event->task->user->email));

        Log::info(sprintf('$s님에게 메일을 보냅니다.', $event->task->user));
/*
        Mail::to($event->task->user)
            ->send(new \App\Mail\TaskCompleted($event->task));
*/
        SendEmailJob::dispatch($event->task);
    }
}
?>

기존의 메일을 발송하는 부분이 Job 클래스의 dispatch() 메소드를 이용하여 메일 발송 Job을 큐에 보내는 코드로 변경되었다. 이렇게 되면 태스크가 완료되는 이벤트가 발생할 때 이메일을 발송하는 Job이 database 큐로 보내지고 사용자의 태스크 완료처리 요청은 바로 완료된다. 


큐로 보내진 Job은 먼저 jobs 테이블에 쌓이게 된다.



아직 이 Job을 처리할 큐 워커가 동작하지 않은 상태이므로 메일발송 Job은 테이블에만 쌓일 뿐 실제 메일은 발송되지 않는다. 다만 메일발송 완료여부와 상관없이 사용자의 태스크 완료처리 요청은 즉시 처리된다. 


이제 데이터베이스에 쌓여있는 Job을 처리하는 큐 워커를 실행해보자. 이 명령에 의해 큐에 쌓인 Job을 처리하는 큐 워커가 실행되면서 실제 메일이 발송되게 된다. 


$ php artisan queue:work
[2018-12-28 15:55:58][6] Processing: App\Jobs\SendEmailJob
[2018-12-28 15:55:58][6] Processed: App\Jobs\SendEmailJob
[2018-12-28 15:55:58][7] Processing: App\Mail\TaskCompleted
[2018-12-28 15:56:01][7] Processed: App\Mail\TaskCompleted


위의 결과화면을 보면 queue worker가 실행되면서 SendEmailJob이 비동기적으로 처리됨을 확인할 수 있다. (SendEmailJob과 TaskCompleted 클래스 모두 비동기처리를 위해 ShouldQueue 인터페이스를 구현한다는 사실을 잊지말자) 이렇게 정상적으로 처리완료된 Job은 jobs 테이블에서 삭제된다. 


큐 워커는 계속 떠있는 상태에서 들어오는 Job을 처리하는 역할을 한다. 만일 운영환경에서 큐를 사용한다면 큐 워커가 비정상적으로 종료되더라도 다시 재기동을 시켜야 하므로 supervisor와 같은 프로세스 모니터링 도구를 사용해야 할 필요가 생길 것이다.  


지금까지 이벤트에서 이메일을 발송하는 Job을 큐로 처리하는 방법에 대해 소개하였다. 여기서는 라라벨 프레임워크에서 이벤트와 리스너가 동작하는 방식에 대해 설명하였고 또 이메일을 발송하는 방법에 대해서도 소개하였다. 그리고 마지막으로 큐를 이용해 비동기적으로 이메일을 발송하도록 함으로써 사용자의 요청에 대한 즉답성을 높일 수 있는 방법에 대해서도 소개하였다. 



Posted by 라스모르
,