Redis를 신뢰하지 않는 Job Scheduler를 만들면서 배운 점

발행: (2026년 2월 9일 오후 09:01 GMT+9)
8 분 소요
원문: Dev.to

Source: Dev.to

‘Redis를 신뢰하지 않는 작업 스케줄러 구축에서 배운 점’ 표지 이미지

Intro

이것은 무엇인가?

Tickr는 Go, Redis, MySQL을 사용하여 만든 백그라운드 작업 스케줄러입니다.
작업을 할당받기 위해 대기하는 워커 풀로 구성됩니다. 작업은 Redis로 구현된 대기 및 준비 큐를 통해 이동하며, 스케줄러에 의해 워커에게 할당됩니다. 모든 것이 동시에 실행되며 이벤트 기반입니다.

왜 만들었나요?

Node.js와 Go로 CRUD 백엔드 API를 만드는 것이 지겨워졌습니다.
엔드포인트와 스키마만이 아니라 동시성, 오류 처리, 시스템 동작에 대해 생각하도록 강요하는, 다르고 낯선 무언가를 작업하고 싶었습니다.

Source:

v1에서 무엇이 깨졌나요?

Polling

v1에서는 스케줄러가 매초 폴링을 통해 작업이 실행될 준비가 되었는지 확인했습니다. 작동은 했지만 비효율적이었습니다. 저는 폴링을 버퍼가 없는 Go 채널과 블로킹 연산으로 교체했습니다—채널은 프로젝트에서 가장 중요한 개념 중 하나가 되었습니다.

Assumptions

많은 것이 자동으로 동작할 것이라고 가정했습니다. 이러한 가정이 깨지면 디버깅과 복구가 어려워졌습니다.

예시: 서버가 종료될 때 아직 대기 큐에 작업이 남아 있다면, 재시작 시 해당 작업들을 복구하는 것이 복잡하고 신뢰할 수 없었습니다.

Redis as single source of truth

초기에는 Redis만을 유일한 진실의 원천으로 사용했습니다. 엣지 케이스 테스트를 진행하면서 불편한 질문들이 떠올랐습니다:

  • 작업이 아직 큐에 남아 있는 상태에서 Redis가 다운되면 어떻게 되나요?
  • Redis가 크래시 후 상태를 잃어버리면 어떻게 되나요?
  • 작업이 중간에 실패하면 어떻게 처리하나요?
  • 완료된 작업에 대한 로그나 히스토리가 필요하면 어떻게 하나요?

이 질문들에 답하기 위해 설계를 재구성했으며, 그 결과 Tickr v2가 탄생했습니다.

Source:

v2 아키텍처

MySQL을 진실의 원천으로

v2에서는 MySQL이 전체 작업 데이터를 저장합니다. JobID와 스케줄링 정보만 Redis에 푸시되며, Redis는 순전히 스케줄링 및 조정을 위해 사용되고 내구성을 담당하지 않습니다.

Architecture diagram

이 접근 방식은:

  • 충돌 시 작업 손실 방지
  • 불필요한 MySQL 폴링 회피
  • 가벼운 ID만으로 큐 간 작업 이동 가능
  • 실행이 필요할 때만 워커가 MySQL에서 전체 작업 데이터를 가져오게 함

스케줄러와 워커

스케줄러는 단일 고루틴(PopReadyQueue)을 실행하며, 작업이 준비될 때까지 Redis에서 블록됩니다.

Redis 연결이 끊긴 경우:

  • 스케줄러는 Redis가 다시 접근 가능해질 때까지 대기합니다.
  • Redis가 상태를 잃었는지 확인합니다.
  • 상태가 손실된 경우, MySQL에서 큐를 재구성합니다.

준비 큐에서 꺼낸 작업은 모든 워커가 수신 대기 중인 버퍼가 없는 작업 채널로 전송됩니다. 채널에 작업이 나타나면 정확히 한 명의 워커가 이를 받아 실행합니다.

작업이 실패한 경우:

  • 지연을 두고 재시도합니다.
  • 재시도 횟수는 제한됩니다(최대 3회).
  • 각 시도마다 지연 시간이 선형적으로 증가합니다.

처리된 엣지 케이스

Redis 충돌

Redis가 다운되면 스케줄러가 일시 중지되고 Redis가 다시 사용 가능해질 때까지 기다립니다. Redis가 복구되면 상태가 손실되었는지 확인하고 필요하면 MySQL에서 복구를 트리거합니다; 그렇지 않으면 정상적으로 계속 진행합니다.

기한이 지난 작업

작업이 예약된 상태에서 Redis가 다운되면 실행 시간이 지나도 트리거되지 않을 수 있습니다. 이전에는 새로운 작업이 큐에 들어올 때까지 이러한 작업이 실행되지 않았습니다. 대기 큐가 다시 채워질 때 채널을 통해 스케줄러에 신호를 보내 기한이 지난 작업을 즉시 재평가하도록 수정했습니다.

실행 중 종료

서버가 작업을 실행 중에 종료될 경우:

  • 워커가 새로운 작업을 받는 것을 중단합니다
  • 진행 중인 작업은 완료될 수 있도록 허용합니다
  • 프로그램은 워커가 종료될 때까지 기다린 후 종료합니다

완료된 작업이 전역 컨텍스트가 이미 취소된 상태라 MySQL에 상태를 업데이트하지 못하는 문제가 발생했습니다. 이를 해결하기 위해 최종 상태 업데이트 전용으로 백그라운드 컨텍스트를 사용하여 종료 중에도 정확성을 보장했습니다.

배운 점

이 프로젝트는 이전에 만든 어떤 것과도 매우 달랐으며, 정말 즐거웠습니다. 저는 다음을 배웠습니다:

  • 상태의 소유권이 왜 중요한지
  • 가정이 얼마나 깨지기 쉬운지
  • 실패를 회피하기보다 그것에 대해 어떻게 논리적으로 접근할 수 있는지
  • 단순히 재시작하는 것이 아니라 복구하는 시스템을 어떻게 설계할 수 있는지
  • 엣지 케이스에서도 쉽게 깨지지 않는 백엔드 코드를 어떻게 작성할 수 있는지

Tickr v2는 어떤 튜토리얼보다 백엔드 시스템에 대해 더 많이 가르쳐 주었습니다.

GitHub 저장소는 여기에서 확인할 수 있습니다: Tickr

Back to Blog

관련 글

더 보기 »