Redis를 신뢰하지 않는 Job Scheduler를 만들면서 배운 점
Source: Dev.to

Intro
What it is?
Tickr는 Go, Redis, MySQL을 사용해 만든 백그라운드 작업 스케줄러입니다.
작업을 할당받기 위해 대기하는 워커 풀로 구성됩니다. 작업은 Redis를 이용해 구현된 대기 및 준비 큐를 통해 이동하며, 스케줄러에 의해 워커에게 할당됩니다. 모든 것이 동시에 실행되고 이벤트‑드리븐 방식으로 동작합니다.
Why did I build it?
Node.js와 Go로 CRUD 백엔드 API를 만드는 것이 점점 지루해졌습니다. 저는 뭔가 다른, 익숙하지 않은 것을 작업하고 싶었고, 이를 통해 엔드포인트와 스키마만이 아니라 동시성, 실패 처리, 시스템 동작에 대해 고민하도록 만들고 싶었습니다.
v1에서 무엇이 깨졌나요?
폴링
v1에서는 스케줄러가 매초 폴링을 하여 작업이 실행될 준비가 되었는지 확인했습니다. 동작은 했지만 비효율적이었습니다. 저는 폴링을 버퍼가 없는 Go 채널과 블로킹 연산으로 교체했습니다—채널은 프로젝트에서 가장 중요한 개념 중 하나가 되었습니다.
가정
많은 것이 자동으로 동작할 것이라고 가정했습니다. 이러한 가정이 깨지면 디버깅과 복구가 어려워졌습니다.
예시: 서버가 종료될 때 작업이 아직 대기열에 남아 있으면, 재시작 시 해당 작업들을 복구하는 것이 복잡하고 신뢰할 수 없었습니다.
Redis를 단일 진실 원천으로 사용
초기에는 Redis만을 유일한 진실 원천으로 의존했습니다. 엣지 케이스 테스트를 진행하면서 불편한 질문들이 떠올랐습니다:
- 작업이 아직 대기 중일 때 Redis가 다운되면 어떻게 될까?
- Redis가 크래시 후 상태를 잃어버리면 어떻게 할까?
- 작업이 중간에 실패하면 어떻게 할까?
- 완료된 작업에 대한 로그나 히스토리가 필요하면 어떻게 할까?
이러한 질문에 답하기 위해 설계가 재구성되었고, 그 결과 Tickr v2가 탄생했습니다.
v2 Architecture
MySQL as the source of truth
v2에서는 MySQL이 전체 작업 데이터를 저장합니다. JobID와 스케줄링 정보만 Redis에 푸시되며, Redis는 순전히 스케줄링 및 조정을 위해 사용되고 내구성을 담당하지 않습니다.

이 접근 방식은 다음을 보장합니다:
- 충돌 시 작업 손실 방지
- 불필요한 MySQL 폴링 회피
- 가벼운 ID만 사용해 큐 간 작업 이동 가능
- 실행이 필요할 때만 작업자가 MySQL에서 전체 작업 데이터를 가져오게 함
Scheduler and Workers
스케줄러는 단일 고루틴(PopReadyQueue)을 실행하며, 작업이 준비될 때까지 Redis에서 블록됩니다.
Redis 연결이 끊긴 경우:
- 스케줄러는 Redis가 다시 접근 가능해질 때까지 대기합니다
- Redis가 상태를 잃었는지 확인합니다
- 상태가 손실된 경우, MySQL에서 큐를 재구성합니다
준비 큐에서 꺼낸 작업은 모든 워커가 수신 대기하는 버퍼가 없는 작업 채널로 전송됩니다. 채널에 작업이 나타나면 정확히 한 명의 워커가 이를 받아 실행합니다.
작업이 실패한 경우:
- 지연을 두고 재시도됩니다
- 재시도 횟수는 제한됩니다(최대 3회)
- 각 시도마다 지연 시간이 선형적으로 증가합니다
처리된 엣지 케이스
Redis 충돌
Redis가 다운되면 스케줄러가 일시 정지하고 Redis가 다시 사용 가능해질 때까지 기다립니다. Redis가 복구되면 상태가 손실되었는지 확인하고 필요하면 MySQL에서 복구를 트리거합니다; 그렇지 않으면 정상적으로 계속 진행합니다.
지연된 작업
Redis가 다운된 상태에서 작업이 예약되어 있으면 실행 시간이 지나도 트리거되지 않을 수 있습니다. 이전에는 새로운 작업이 큐에 들어올 때까지 이러한 작업이 실행되지 않았습니다. 대기 큐가 다시 채워질 때 채널을 통해 스케줄러에 신호를 보내 즉시 지연된 작업을 재평가하도록 수정했습니다.
실행 중인 상태에서 종료
서버가 작업을 실행하는 도중에 종료되는 경우:
- 워커는 새로운 작업을 받지 않음
- 진행 중인 작업은 완료될 수 있도록 허용
- 프로그램은 워커가 끝날 때까지 기다린 후 종료
완료된 작업이 전역 컨텍스트가 이미 취소된 상태라 MySQL에 상태를 업데이트하지 못하는 문제가 있었습니다. 최종 상태 업데이트 전용으로 백그라운드 컨텍스트를 사용하도록 수정하여 종료 중에도 정확히 업데이트되도록 했습니다.
배운 점
이 프로젝트는 내가 이전에 만든 어떤 것과도 매우 달랐으며, 나는 그것을 많이 즐겼다. 나는 다음을 배웠다:
- 상태 소유권이 왜 중요한지
- 가정이 얼마나 깨지기 쉬운지
- 실패를 피하기보다 그것에 대해 어떻게 사고할지
- 단순히 재시작하는 대신 복구하는 시스템을 어떻게 설계할지
- 엣지 케이스에서도 쉽게 깨지지 않는 백엔드 코드를 어떻게 작성할지
Tickr v2는 어떤 튜토리얼보다 백엔드 시스템에 대해 더 많이 가르쳐 주었다.
GitHub 저장소는 여기에서 확인할 수 있다: Tickr