메세징 대행사 전환으로 인한 발생한 문제: SideKiq을 활용한 Ruby On Rails에서의 백그라운드 작업 처리

Hyunta·2024년 1월 5일
0
post-custom-banner

문제 상황

메세징 대행사를 A 대행사(기존)에서 B 대행사(신규)로 전환하는 작업을 진행했습니다. 대행사로 메세지 전송을 요청하고 응답받는 과정에서 대행사간의 차이가 있어서 문제가 발생했습니다.

(기존) A 대행사의 방식:

(신규) B 대행사의 방식:

A 대행사의 경우 처리 결과를 상세하게 알려줍니다. 알림톡으로 요청했을 경우 실제로 알림톡이 전달되었는지, 아니면 어떤 이유에 의해서 SMS 혹은 LMS로 전달되었는지 Response로 즉각 응답을 해줍니다.

반면에 B 대행사는 처리 결과를 자세하게 알려주지 않습니다. 메세지 템플릿, 파라미터에 대한 검증 정도만 이뤄지고 실제 발송 유무와 관계없이 B 대행사와의 Connection이 정상적으로 이뤄지게되면 무조건 성공했다고 response가 오도록 되어있었습니다. 전송 수단에 대해서도 전혀 알 수 없었습니다. 대신 B 대행사는 리포트를 추후에 조회할 수 있습니다. 리포트는 상세한 처리 결과에 대해서 응답을 내려줍니다.

발송된 메세지를 정산할 때 DB에 저장된 발송 내역을 합산하여 청구합니다. 기존 A대행사를 사용할 때는 즉각적인 Response가 왔기 때문에 DB 값을 신뢰할 수 있었지만 B 대행사로 변경하게 되면서 DB 값을 항상 신뢰할 수 없게 되었습니다. 추가로 Report를 조회하는 작업이 필요했고 이를 기반으로 DB 값을 업데이트하는 과정이 필요해졌습니다.

Java + Spring 환경에서 이 문제를 해결하려면 @AsyncScheduledExecutorService를 사용해서 메세지 발송 요청이 이뤄지고 난 뒤에 특정 시간 뒤에 report를 조회해서 DB를 업데이트 하는 방식으로 구현했을 것 같습니다. 하지만 저희는 Ruby On Rails 환경의 코드가 작성되어있기 때문에 같이 사용하고 있는 SideKiq을 이용해서 구현했습니다.

SideKiq에 대한 간단한 소개

SideKiq은 Ruby On Rails에서 백그라운드 작업을 처리하는 라이브러리입니다. 멀티 스레딩을 지원하여 효율적으로 서버를 활용할 수 있도록 도와주고, 복잡하거나 시간이 오래 걸리는 작업을 백그라운드에서 처리할 수 있게 됩니다.

Redis를 같이 이용해 백그라운드 작업을 작업큐로 저장합니다. 개별 작업은 Ruby 객체로 구성되며, 이 객체는 Redis에 저장되기 전에 JSON으로 직렬화됩니다. 반대로 작업이 처리될 때, 이 JSON 데이터는 다시 Ruby 객체로 역직렬화되어 작업이 수행됩니다.

Sidekiq의 작업 큐는 Redis의 리스트 데이터 구조를 사용하며, 다양한 Sidekiq 작업자가 이 큐를 모니터링하고 작업을 가져와 처리합니다. 또한, Sidekiq은 실패한 작업을 추적하고 재시도하기 위해 Redis의 세트 데이터 구조를 사용합니다.

메세지 발송 후 Job 생성

A 대행사와 마찬가지로 B 대행사에 메세지 발송 요청을 보냅니다. 반환되는 Response를 이용해서 연결에 성공했을 경우 Job을 생성하는 메서드를 만들어줬습니다. Job이란 SideKiq의 큐에서 처리하는 작업의 단위입니다.

코드를 전부 보여드릴 수는 없지만 MessageSender 는 대략 아래와 같은 흐름으로 작동하게 됩니다.

    def send_message
      response = execute_request(get_headers, params)
      process_response(response)
    end


    def process_response(response)
        message_log = create_message_log(response)
        UpdateMessageLogJob.perform_in(10.seconds, message_log.id, Time.now.strftime("%Y-%m-%d"))
    end

메세지 전송을 요청에 성공하면 message_log를 생성하고 UpdateMessageLogJob 이라는 ActiveJob을 큐에 생성합니다. 메세지가 실제로 발송하는데까지 약간의 딜레이가 있기 때문에 10초의 딜레이를 주고 실행할 수 있도록 설정했습니다.

기존에는 message_log 에서 메세지 발송 성공 여부를 MessageSender 에서 알 수 있었지만 B 대행사로 바뀌게 되면서 상태를 여기서 결정할 수 없게 됐습니다. 따라서 메세지 발송중 이라는 상태로 초기에 설정해놓고 Job에서 결과를 조회한 뒤에 업데이트를 해주는 방식으로 변경했습니다.

만약 리포트 조회에 실패한다면?

UpdateMessageLogJob 을 통해서 리포트를 조회하고, 필요한 정보를 추출합니다. 그리고 성공 여부에 따라서 messag_log 를 업데이트하는 작업을 수행합니다.

class UpdateMessageLogJob
  include Sidekiq::Job

  def perform(message_log_id, request_date)
      response = fetch_report()
      sent_message_info = extract_sent_message_info(response)
      update_message_log_with_report(sent_message_info, message_log)
  end
end

하지만 이 Job은 실패할 가능성이 있습니다. fetch_report() 메서드를 호출할 때 B 대행사에 문제가 발생해서 요청이 정상적으로 이뤄지지 않을 수도 있고, 처리가 지연돼서 리포트가 안만들어졌을 수도 있습니다. SideKiq의 Job에서는 이런 상황에 재시도할 수 있도록 하는 기능을 지원합니다.

작업 중 예외가 발생하면 Sidekiq은 자체적인 재시도 메커니즘을 이용해 해당 작업을 재실행합니다. 따라서 실패한 작업도 안전하게 복구할 수 있습니다.

하지만 만약 최대 재시도 횟수동안 문제가 해결되지 않는다면, Sidekiq은 재시도를 멈추고 작업을 'Dead set'으로 이동시킵니다. 이후에는 수동으로 작업을 재시도할 수 있습니다. 단, 6개월 동안 재시도되지 않은 작업은 Sidekiq에 의해 자동으로 삭제됩니다.

class UpdateMessageLogJob
  include Sidekiq::Job
  sidekiq_options retry: 5

  sidekiq_retries_exhausted do |job|
    message_log_id = job['args'].first
    MessageLog.find(message_log_id).timeout!
  end

  def perform(message_log_id, request_date)
      response = fetch_report()
      sent_message_info = extract_sent_message_info(response)
      update_message_log_with_report(sent_message_info, message_log)
  end
end

재시도 횟수의 기본 값은 25회인데 너무 많아서, 5회로 줄였습니다. 추가로 재시도 횟수가 초과됐을 경우에는 MessageLog가 계속 발송중 인 상태로 남아있게 되는 문제가 생기기 때문에 시간초과 라는 상태로 업데이트 할 수 있도록 기능을 추가했습니다.

Sidekiq에서는 재시도 횟수가 초과되면 정의된 sidekiq_retries_exhausted Hook을 호출합니다. 이 Hook은 큐에 있는 작업 해시를 인수로 받아, Sidekiq이 작업을 Dead set으로 이동시키기 직전에 호출됩니다. 이를 통해 재시도 횟수가 초과된 작업에 대해 추가적인 작업을 수행할 수 있습니다.

후기

익숙한 Java와 Spring으로 계속 개발을 하다 Ruby로 개발을 시작하게 되면서 시야가 넓어짐을 느끼고 있습니다. 이번에 작업을 진행하면서 SideKiq의 다양한 기능을 사용해볼 수 있었고 재시도 처리를 하면서 Rails와 SideKiq의 작동방식에 대해서 파악할 수 있는 계기가 되었습니다.

마지막 재시도 횟수 초과시 함수를 호출하는 과정에서 Rails쪽 메서드를 사용하게되어 비정상적으로 작동하고 있었는데, 이를 모르고 이틀동안 왜 안될까 고민하면서 어쩌다보니 SideKiq의 Job, Queue의 구조를 파악하면서 공부할 수 있었습니다. 알면 알수록 Ruby라는 언어와 Rails의 생태계는 정말 잘 만들어졌다는 생각이 드는 것 같습니다.

profile
세상을 아름답게!
post-custom-banner

0개의 댓글