액티브 잡 기본 사항
이 가이드는 백그라운드 작업을 생성, 대기열에 넣기 및 실행하는 데 필요한 모든 것을 제공합니다.
이 가이드를 읽고 나면 다음을 알 수 있습니다:
- 작업을 생성하는 방법
- 작업을 대기열에 넣는 방법
- 백그라운드에서 작업을 실행하는 방법
- 애플리케이션에서 비동기적으로 이메일을 보내는 방법
액티브 잡이란 무엇인가?
액티브 잡은 다양한 대기열 백엔드에서 작업을 선언하고 실행할 수 있는 프레임워크입니다. 이러한 작업은 정기적인 정리, 청구 요금, 메일링 등 다양할 수 있습니다. 작은 단위의 작업으로 나눌 수 있고 병렬로 실행할 수 있는 모든 것이 해당됩니다.
액티브 잡의 목적
주요 목적은 모든 Rails 애플리케이션에 작업 인프라를 갖추도록 하는 것입니다. 그러면 프레임워크 기능과 다른 젬들이 API 차이에 대해 걱정하지 않고 그 위에 구축할 수 있습니다. 큐잉 백엔드를 선택하는 것은 운영상의 문제가 됩니다. 그리고 작업을 다시 작성하지 않고도 백엔드를 전환할 수 있습니다.
참고: Rails는 기본적으로 프로세스 내부 스레드 풀을 사용하여 작업을 비동기적으로 실행하는 구현을 제공합니다. 작업은 비동기적으로 실행되지만, 대기열의 작업은 재시작 시 삭제됩니다.
작업 생성 및 대기열에 넣기
이 섹션에서는 작업을 생성하고 대기열에 넣는 단계별 가이드를 제공합니다.
작업 생성하기
액티브 잡은 작업을 생성하기 위한 Rails 생성기를 제공합니다. 다음 명령은 app/jobs
에 작업을 생성하고 (test/jobs
에 관련 테스트 케이스를 생성합니다):
$ bin/rails generate job guests_cleanup invoke test_unit create test/jobs/guests_cleanup_job_test.rb create app/jobs/guests_cleanup_job.rb
특정 대기열에서 실행되는 작업을 생성할 수도 있습니다:
$ bin/rails generate job guests_cleanup --queue urgent
생성기를 사용하지 않으려면 app/jobs
내에 직접 파일을 만들면 됩니다. 단, ApplicationJob
을 상속해야 합니다.
작업의 모습은 다음과 같습니다:
class GuestsCleanupJob < ApplicationJob queue_as :default def perform(*guests) # 나중에 무언가를 합니다 end end
perform
에 원하는 만큼의 인수를 정의할 수 있습니다.
추상 클래스가 이미 있고 이름이 ApplicationJob
과 다른 경우, --parent
옵션을 사용하여 다른 추상 클래스를 지정할 수 있습니다:
$ bin/rails generate job process_payment --parent=payment_job
class ProcessPaymentJob < PaymentJob queue_as :default def perform(*args) # 나중에 무언가를 합니다 end end
작업 대기열에 넣기
perform_later
와 선택적으로 set
을 사용하여 작업을 대기열에 넣습니다. 다음과 같이 사용합니다:
# 큐잉 시스템을 사용할 수 있게 되면 즉시 실행되도록 작업을 대기열에 넣습니다. GuestsCleanupJob.perform_later guest
# 내일 정오에 실행되도록 작업을 대기열에 넣습니다. GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
# 1주일 후에 실행되도록 작업을 대기열에 넣습니다. GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
# `perform_now`와 `perform_later`는 내부적으로 `perform`을 호출하므로 `perform`에 정의된 만큼의 인수를 전달할 수 있습니다. GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')
끝!
대량으로 작업 대기열에 넣기
perform_all_later
를 사용하여 여러 작업을 한 번에 대기열에 넣을 수 있습니다. 자세한 내용은 대량 대기열 넣기를 참조하세요.
작업 실행
프로덕션에서 작업을 대기열에 넣고 실행하려면 대기열 백엔드를 설정해야 합니다. 즉, Rails에서 사용할 3rd-party 대기열 라이브러리를 선택해야 합니다. Rails 자체는 프로세스 내부 대기열 시스템만 제공하며, 이는 작업을 RAM에만 보관합니다. 프로세스가 충돌하거나 머신이 재설정되면 기본 async 백엔드의 모든 미처리 작업이 손실됩니다. 이는 작은 애플리케이션이나 중요하지 않은 작업에는 괜찮을 수 있지만, 대부분의 프로덕션 애플리케이션에서는 지속성 있는 백엔드를 선택해야 합니다.
백엔드
액티브 잡은 여러 대기열 백엔드(Sidekiq, Resque, Delayed Job 등)에 대한 내장 어댑터를 제공합니다. 최신 어댑터 목록은 ActiveJob::QueueAdapters
API 문서를 참조하세요.
백엔드 설정하기
config.active_job.queue_adapter
를 사용하여 대기열 백엔드를 쉽게 설정할 수 있습니다:
# config/application.rb module YourApp class Application < Rails::Application # 어댑터의 젬이 Gemfile에 있고 어댑터 특정 설치 및 배포 지침을 따르는지 확인하세요. config.active_job.queue_adapter = :sidekiq end end
작업별로 백엔드를 구성할 수도 있습니다:
class GuestsCleanupJob < ApplicationJob self.queue_adapter = :resque # ... end # 이제 이 작업은 `config.active_job.queue_adapter`에서 설정한 것을 재정의하여 `resque`를 백엔드 대기열 어댑터로 사용합니다.
백엔드 시작하기
작업은 Rails 애플리케이션과 병렬로 실행되므로 대부분의 대기열 라이브러리에서는 작업 처리를 위해 라이브러리 특정 대기열 서비스를 시작해야 합니다(Rails 앱을 시작하는 것 외에). 라이브러리 문서를 참조하여 큐 백엔드를 시작하는 방법을 확인하세요.
다음은 문서 목록입니다:
대기열
대부분의 어댑터는 여러 대기열을 지원합니다. 액티브 잡에서는 queue_as
를 사용하여 특정 대기열에서 작업을 실행하도록 예약할 수 있습니다:
class GuestsCleanupJob < ApplicationJob queue_as :low_priority # ... end
application.rb
에서 config.active_job.queue_name_prefix
를 사용하여 모든 작업의 대기열 이름에 접두사를 붙일 수 있습니다:
# config/application.rb module YourApp class Application < Rails::Application config.active_job.queue_name_prefix = Rails.env end end
# app/jobs/guests_cleanup_job.rb class GuestsCleanupJob < ApplicationJob queue_as :low_priority # ... end # 이제 프로덕션 환경에서는 production_low_priority 대기열에서, 스테이징 환경에서는 staging_low_priority 대기열에서 작업이 실행됩니다.
작업별로 접두사를 구성할 수도 있습니다.
class GuestsCleanupJob < ApplicationJob queue_as :low_priority self.queue_name_prefix = nil # ... end # 이제 작업의 대기열에 접두사가 붙지 않습니다. `config.active_job.queue_name_prefix`에서 설정한 것을 재정의합니다.
기본 대기열 이름 접두사 구분 기호는 ‘_'입니다. application.rb
에서 config.active_job.queue_name_delimiter
를 설정하여 변경할 수 있습니다:
# config/application.rb module YourApp class Application < Rails::Application config.active_job.queue_name_prefix = Rails.env config.active_job.queue_name_delimiter = '.' end end
# app/jobs/guests_cleanup_job.rb class GuestsCleanupJob < ApplicationJob queue_as :low_priority # ... end # 이제 프로덕션 환경에서는 production.low_priority 대기열에서, 스테이징 환경에서는 staging.low_priority 대기열에서 작업이 실행됩니다.
작업 수준에서 대기열을 제어하려면 queue_as
블록을 전달할 수 있습니다. 블록은 작업 컨텍스트에서 실행되므로(self.arguments
접근 가능) 대기열 이름을 반환해야 합니다:
class ProcessVideoJob < ApplicationJob queue_as do video = self.arguments.first if video.owner.premium? :premium_videojobs else :videojobs end end def perform(video) # 비디오 처리 end end
ProcessVideoJob.perform_later(Video.last)
작업이 실행될 대기열을 더 세밀하게 제어하려면 set
에 :queue
옵션을 전달할 수 있습니다:
MyJob.set(queue: :another_queue).perform_later(record)
참고: 대기열 백엔드가 대기열 이름을 “듣고” 있는지 확인하세요. 일부 백엔드에서는 청취할 대기열을 지정해야 합니다.
우선순위
일부 어댑터는 작업 계속해서 한국어 번역문은 다음과 같습니다:
우선순위
일부 어댑터는 작업 수준에서 우선순위를 지원하여 대기열 내부 또는 전체 대기열에서 작업을 상대적으로 우선순위화할 수 있습니다.
queue_with_priority
를 사용하여 특정 우선순위로 작업을 예약할 수 있습니다:
class GuestsCleanupJob < ApplicationJob queue_with_priority 10 # ... end
이 기능은 우선순위를 지원하지 않는 어댑터에서는 효과가 없습니다.
queue_as
와 마찬가지로 queue_with_priority
에 작업 컨텍스트에서 평가되는 블록을 전달할 수도 있습니다:
class ProcessVideoJob < ApplicationJob queue_with_priority do video = self.arguments.first if video.owner.premium? 0 else 10 end end def perform(video) # 비디오 처리 end end
ProcessVideoJob.perform_later(Video.last)
set
에 :priority
옵션을 전달할 수도 있습니다:
MyJob.set(priority: 50).perform_later(record)
콜백
액티브 잡은 작업 수명 주기 동안 로직을 트리거할 수 있는 훅을 제공합니다. Rails의 다른 콜백과 마찬가지로, 일반 메서드로 구현하고 매크로 스타일 클래스 메서드를 사용하여 콜백으로 등록할 수 있습니다:
class GuestsCleanupJob < ApplicationJob queue_as :default around_perform :around_cleanup def perform # 나중에 무언가를 합니다 end private def around_cleanup # 수행 전에 무언가를 합니다 yield # 수행 후에 무언가를 합니다 end end
매크로 스타일 클래스 메서드는 블록을 받을 수도 있습니다. 블록 내부의 코드가 한 줄로 충분할 경우 이 스타일을 고려해 볼 수 있습니다. 예를 들어, 모든 작업이 대기열에 추가될 때마다 메트릭을 보낼 수 있습니다:
class ApplicationJob < ActiveJob::Base before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" } end
사용 가능한 콜백
perform_all_later
를 사용하여 작업을 대량으로 대기열에 넣을 때는 around_enqueue
와 같은 콜백이 개별 작업에 트리거되지 않습니다. 대량 대기열 넣기 콜백을 참조하세요.
대량 대기열 넣기
perform_all_later
를 사용하여 여러 작업을 한 번에 대기열에 넣을 수 있습니다. 대량 대기열 넣기는 대기열 데이터 저장소(Redis 또는 데이터베이스)에 대한 왕복 횟수를 줄여 개별적으로 동일한 작업을 대기열에 넣는 것보다 더 효율적인 작업입니다.
perform_all_later
는 액티브 잡의 최상위 API입니다. 인스턴스화된 작업을 인수로 받습니다(이는 perform_later
와 다릅니다). perform_all_later
는 내부적으로 perform
을 호출합니다. new
에 전달된 인수는 perform
이 실행될 때 전달됩니다.
GuestCleanupJob
인스턴스를 perform_all_later
에 전달하는 예시는 다음과 같습니다:
# `GuestsCleanupJob` 인스턴스를 생성하여 `perform_all_later`에 전달합니다. # `new`의 인수는 `perform`으로 전달됩니다. guest_cleanup_jobs = Guest.all.map { |guest| GuestsCleanupJob.new(guest) } # `GuestsCleanupJob` 인스턴스별로 별도의 작업을 대기열에 넣습니다. ActiveJob.perform_all_later(guest_cleanup_jobs) # `set` 메서드를 사용하여 옵션을 구성한 다음 작업을 대량으로 대기열에 넣을 수도 있습니다. guest_cleanup_jobs = Guest.all.map { |guest| GuestsCleanupJob.new(guest).set(wait: 1.day) } ActiveJob.perform_all_later(guest_cleanup_jobs)
perform_all_later
는 성공적으로 대기열에 넣은 작업 수를 기록합니다. 예를 들어 위의 Guest.all.map
이 3개의 guest_cleanup_jobs
를 생성한 경우 “Async에 3개의 작업을 대기열에 넣었습니다(3개의 GuestsCleanupJob)"와 같이 기록합니다(모두 대기열에 넣어졌다고 가정).
perform_all_later
의 반환 값은 nil
입니다. 이는 perform_later
가 대기열에 넣은 작업 클래스의 인스턴스를 반환하는 것과 다릅니다.
다양한 액티브 잡 클래스 대기열에 넣기
perform_all_later
를 사용하면 서로 다른 액티브 잡 클래스 인스턴스를 동일한 호출에서 대기열에 넣을 수 있습니다. 예:
class ExportDataJob < ApplicationJob def perform(*args) # 데이터 내보내기 end end class NotifyGuestsJob < ApplicationJob def perform(*guests) # 게스트에게 이메일 보내기 end end # 작업 인스턴스 생성 cleanup_job = GuestsCleanupJob.new(guest) export_job = ExportDataJob.new(data) notify_job = NotifyGuestsJob.new(guest) # 여러 클래스의 작업 인스턴스를 한 번에 대기열에 넣습니다. ActiveJob.perform_all_later(cleanup_job, export_job, notify_job)
대량 대기열 넣기 콜백
perform_all_later
를 사용하여 작업을 대량으로 대기열에 넣을 때는 around_enqueue
와 같은 콜백이 개별 작업에 트리거되지 않습니다. 이 동작은 다른 Active Record 대량 메서드와 일치합니다. 콜백은 개별 작업에서 실행되므로 이 메서드의 대량 특성을 활용할 수 없습니다.
그러나 perform_all_later
메서드는 ActiveSupport::Notifications
를 사용하여 구독할 수 있는 enqueue_all.active_job
이벤트를 발생시킵니다.
successfully_enqueued?
메서드를 사용하여 특정 작업이 성공적으로 대기열에 넣어졌는지 확인할 수 있습니다.
대기열 백엔드 지원
perform_all_later
의 경우 대량 대기열 넣기는 대기열 백엔드에 의해 지원되어야 합니다.
예를 들어, Sidekiq에는 Redis에 많은 작업을 밀어넣고 왕복 네트워크 지연 시간을 방지할 수 있는 push_bulk
메서드가 있습니다. GoodJob도 GoodJob::Bulk.enqueue
메서드를 통해 대량 대기열 넣기를 지원합니다. 새로운 대기열 백엔드 Solid Queue
도 대량 대기열 넣기 지원을 추가했습니다.
대기열 백엔드가 대량 대기열 넣기를 지원하지 않는 경우 perform_all_later
는 작업을 하나씩 대기열에 넣습니다.
액션 메일러
현대 웹 애플리케이션에서 가장 일반적인 작업 중 하나는 요청-응답 주기 외부에서 이메일을 보내는 것이므로 사용자가 기다릴 필요가 없습니다. 액티브 잡은 액션 메일러와 통합되어 쉽게 비동기적으로 이메일을 보낼 수 있습니다:
# 지금 이메일을 보내려면 #deliver_now를 사용하세요 UserMailer.welcome(@user).deliver_now # 액티브 잡을 통해 이메일을 보내려면 #deliver_later를 사용하세요 UserMailer.welcome(@user).deliver_later
참고: Rake 작업(예: .deliver_later
를 사용하여 이메일 보내기)에서 비동기 대기열을 사용하면 일반적으로 작동하지 않습니다. Rake는 .deliver_later
이메일이 모두 처리되기 전에 종료될 수 있기 때문입니다. 이 문제를 해결하려면 .deliver_now
를 사용하거나 개발 환경에서 지속적인 대기열을 실행하세요.
국제화
각 작업은 작업이 생성될 때 설정된 I18n.locale
을 사용합니다. 이는 비동기적으로 이메일을 보내는 경우 유용합니다:
I18n.locale = :eo UserMailer.welcome(@user).deliver_later # 이메일은 에스페란토로 지역화됩니다.
인수에 대한 지원 유형
액티브 잡은 기본적으로 다음과 같은 유형의 인수를 지원합니다:
- 기본 유형(
NilClass
,String
,Integer
,Float
,BigDecimal
,TrueClass
,FalseClass
) Symbol
Date
Time
DateTime
ActiveSupport::TimeWithZone
ActiveSupport::Duration
Hash
(키는String
또는Symbol
유형이어야 함)ActiveSupport::HashWithIndifferentAccess
Array
Range
Module
Class
GlobalID
액티브 잡은 GlobalID를 인수로 지원합니다. 이를 통해 클래스/ID 쌍 대신 실제 Active Record 객체를 작업에 전달할 수 있으므로 수동으로 역직렬화할 필요가 없습니다. 이전에는 작업이 다음과 같이 보였습니다:
class TrashableCleanupJob < ApplicationJob def perform(trashable_class, trashable_id, depth) trashable = trashable_class.constantize.find(trashable_id) trashable.cleanup(depth) end end
이제 다음과 같이 할 수 있습니다:
class TrashableCleanupJob < ApplicationJob def perform(trashable, depth) trashable.cleanup(depth) end end
이는 GlobalID::Identification
을 믹스인한 모든 클래스(기본적으로 Active Record 클래스)에 적용됩니다계속해서 한국어 번역문은 다음과 같습니다:
직렬화기
지원되는 인수 유형 목록을 확장할 수 있습니다. 자신만의 직렬화기를 정의하면 됩니다:
# app/serializers/money_serializer.rb class MoneySerializer < ActiveJob::Serializers::ObjectSerializer # 인수가 이 직렬화기에 의해 직렬화되어야 하는지 확인합니다. def serialize?(argument) argument.is_a? Money end # 지원되는 객체 유형을 사용하여 객체를 더 단순한 대표로 변환합니다. # 권장되는 대표는 특정 키가 있는 해시입니다. 키는 기본 유형만 가능합니다. # `super`를 호출하여 사용자 정의 직렬화기 유형을 해시에 추가해야 합니다. def serialize(money) super( "amount" => money.amount, "currency" => money.currency ) end # 직렬화된 값을 적절한 객체로 변환합니다. def deserialize(hash) Money.new(hash["amount"], hash["currency"]) end end
그리고 이 직렬화기를 목록에 추가합니다:
# config/initializers/custom_serializers.rb Rails.application.config.active_job.custom_serializers << MoneySerializer
초기화 중에 재로드 가능한 코드를 자동 로드하는 것은 지원되지 않습니다. 따라서 직렬화기를 한 번만 로드되도록 설정하는 것이 좋습니다. 예를 들어 config/application.rb
를 다음과 같이 수정할 수 있습니다:
# config/application.rb module YourApp class Application < Rails::Application config.autoload_once_paths << Rails.root.join('app', 'serializers') end end
예외
작업 실행 중 발생한 예외는 rescue_from
으로 처리할 수 있습니다:
class GuestsCleanupJob < ApplicationJob queue_as :default rescue_from(ActiveRecord::RecordNotFound) do |exception| # 예외로 무언가를 합니다 end def perform # 나중에 무언가를 합니다 end end
작업에서 예외가 처리되지 않으면 "실패한” 것으로 간주됩니다.
실패한 작업 재시도 또는 삭제
실패한 작업은 별도로 구성하지 않는 한 재시도되지 않습니다.
retry_on
또는 discard_on
을 사용하여 실패한 작업을 재시도하거나 삭제할 수 있습니다. 예:
class RemoteServiceJob < ApplicationJob retry_on CustomAppException # 기본값은 3초 대기, 5회 시도 discard_on ActiveJob::DeserializationError def perform(*args) # CustomAppException 또는 ActiveJob::DeserializationError 발생 가능 end end
역직렬화
GlobalID를 통해 #perform
에 전달된 전체 Active Record 객체를 직렬화할 수 있습니다.
작업이 대기열에 넣어진 후 #perform
메서드가 호출되기 전에 전달된 레코드가 삭제된 경우 액티브 잡은 ActiveJob::DeserializationError
예외를 발생시킵니다.
작업 테스트
작업을 테스트하는 방법에 대한 자세한 지침은 테스팅 가이드에서 확인할 수 있습니다.
디버깅
작업이 어디에서 오는지 파악하는 데 도움이 필요한 경우 자세한 로깅을 활성화할 수 있습니다.