시간대별 뉴스레터 발송: 견고하고 멱등적인 스케줄링 전략

Actually doing things in user's time zone - Julik Tarkhanov

작성자
Ruby Weekly
발행일
2025년 10월 01일

핵심 요약

  • 1 사용자 시간대에 맞춰 뉴스레터를 정확히 발송하기 위한 세 가지 스케줄링 접근 방식(PostgreSQL, Fugit, 모델 기반)을 비교 분석합니다.
  • 2 ActiveJob의 중복 실행 문제를 해결하고 안정적인 뉴스레터 발송을 보장하기 위해 멱등성(idempotency)과 `last_delivered_at` 기록의 중요성을 강조합니다.
  • 3 뉴스레터 자체를 모델링하여 배송 시간을 관리하고 다음 뉴스레터를 즉시 생성하는 방식이 가장 견고하며, UTC 사용 및 올바른 모델링을 권장합니다.

도입

이전 시간대 관련 글에 이어, 사용자 시간대에 맞춰 매일 아침 뉴스레터를 안정적으로 발송하는 실제적인 방법에 대한 질문에 답합니다. 이 글에서는 UTC 시간을 기준으로 배송 시간을 결정한 후, 실제 작업을 수행하는 여러 접근 방식을 소개하며, 특히 필자가 선호하는 모델 기반 방식을 포함하여 견고한 스케줄링 전략을 탐구합니다. 이는 복잡한 시간대 문제를 넘어 실제 서비스 운영의 안정성을 확보하는 데 중점을 둡니다.

1. PostgreSQL 활용 접근 방식

  • IANA 시간대 식별자: time_zone을 IANA 식별자로 저장하여 PostgreSQL의 내장 시간대 변환 기능을 활용합니다.

  • AT TIME ZONE: TIMESTAMP '...' AT TIME ZONE 'Europe/Moscow'와 같이 SQL 쿼리 내에서 시간대 변환을 직접 수행할 수 있습니다.

  • UTC 변환: (timestamp AT TIME ZONE user_timezone) AT TIME ZONE 'UTC' 형태로 사용자 시간대의 특정 시간을 UTC로 정확히 변환하여 저장합니다.

  • 다음 배송 시간 계산: CURRENT_DATEINTERVAL '1 day'를 사용하여 오늘 또는 내일의 UTC 배송 시간을 계산하고, NOW() AT TIME ZONE 'UTC'와 비교하여 가장 가까운 미래 시간을 선택합니다. PostgreSQL은 일광 절약 시간(DST) 전환도 자동으로 처리합니다.

  • 주의사항: 데이터베이스 서버 및 세션의 시간대 설정에 따라 표시되는 UTC 오프셋이 다를 수 있으므로, 데이터베이스를 UTC로 설정하는 것이 가장 좋습니다.

2. Fugit (Ruby Gem) 활용 접근 방식

  • Rails 내 구현: PostgreSQL 방식과 유사하게 Rails 애플리케이션 내에서 Fugit gem을 사용하여 시간대 변환 및 다음 배송 시간을 계산합니다.

  • 가독성 및 유연성: SQL보다 가독성이 높고, 여러 이벤트 패턴을 하나의 크론(cron) 형식으로 처리할 수 있는 유연성이 있습니다.

  • 성능: PostgreSQL 방식에 비해 상대적으로 느릴 수 있습니다.

  • 코드 예시: Fugit.do_parse_cronish(pattern)을 통해 크론 객체를 생성하고, cron.next_time(_reference = Time.current).utc로 다음 UTC 시간을 얻어 ActiveJobset(wait_until: ...)에 활용합니다.

3. 중복 실행 방지 및 기록의 필요성

  • ActiveJob의 한계: ActiveJob은 작업의 고유한 식별 개념이 부족하여, 사용자 설정 변경 시 동일한 뉴스레터가 여러 번 큐에 추가될 수 있습니다.

  • last_delivered_at: newsletter_last_delivered_at 컬럼을 사용하여 최근에 발송된 뉴스레터를 건너뛰는 방식으로 중복을 방지할 수 있습니다.

  • 문제점: 이 방식은 SQL 쿼리와 DeliverNewsletterJob 내부 모두에서 중복 확인 로직이 필요하며, 복잡성과 오류 가능성을 증가시킵니다.

4. 모델 기반 접근 방식 (선호)

  • Newsletter 모델 도입: Account와는 별개로 Newsletter 모델을 도입하여 뉴스레터 자체를 독립적인 엔티티로 관리합니다.

  • 상태 관리: Newsletter 모델은 pending, delivering, delivered와 같은 상태를 가집니다.

  • 멱등성 키(Idempotency Key): idempotency_key를 사용하여 deliver_at 값이 변경되어 이전 작업이 여전히 큐에 남아있을 경우, 중복 실행을 방지합니다. 작업 실행 시 idempotency_key를 확인하여 유효한 작업만 처리합니다.

  • 다음 뉴스레터 즉시 생성: 현재 뉴스레터가 delivering 상태로 전환될 때, 다음 뉴스레터를 즉시 pending 상태로 생성하여 연속성을 확보합니다.

  • 코드 구조:

    • Account 모델에 has_many :newsletters 관계 설정 및 deliver_next_newsletter_at 메서드 정의.
    • Newsletter 모델에 enum :state, belongs_to :account, idempotency_key 관리 로직, perform_delivery! 메서드 구현.
    • NewsletterDeliveryJob은 `Newsletter

perform_delivery!`를 호출.

  • 장점:
    • 명확한 기록: 사용자별 뉴스레터 수신 기록을 시간 순서대로 추적할 수 있습니다.
    • 확장성: 뉴스레터에 기사(Article)를 연결하는 등 추가 기능을 쉽게 구현할 수 있습니다.
    • 견고성: 멱등성 키를 통해 중복 배송 문제를 효과적으로 방지합니다.
  • 주의사항: ActiveRecord 콜백을 포함하는 복잡한 시스템에서는 미묘한 경합 조건(race conditions)이 발생할 수 있으므로 신중한 설계가 필요합니다.

결론

단순히 '언제' 작업을 수행할지 결정하는 것을 넘어, '어떻게' 작업을 견고하게 실행할지가 중요합니다. 특히 중복 실행을 방지하는 멱등성 개념은 필수적입니다. 데이터베이스, 서버, 애플리케이션 모두 UTC를 사용하도록 구성하는 것이 권장되며, 뉴스레터와 같은 스케줄링 요소를 독립적인 모델로 잘 표현하는 것이 장기적으로 안정적이고 확장 가능한 시스템을 구축하는 데 핵심적인 역할을 합니다. 이는 시스템의 투명성과 유지보수성을 크게 향상시킬 것입니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!