1. Delayed Job의 한계: ‘하나의 계산기’ 문제
기존 시스템에서 사용하던 Delayed Job은 데이터베이스의 행 잠금(Row Locks)을 통해 작업을 가져옵니다. 워커의 수를 늘리면 처리량이 늘어날 것으로 기대하지만, 실제로는 여러 워커가 동일한 행을 잠그기 위해 경쟁하면서 병목 현상이 발생합니다. 발표자는 이를 ‘여러 명의 점원이 하나의 계산기를 공유하는 상황’에 비유했습니다. 한 워커가 트랜잭션을 완료할 때까지 다른 워커들은 대기 상태에 머물게 되어, 결과적으로 워커를 추가해도 전체 처리량은 정체되는 현상이 나타납니다.
2. 비동기 작업의 두 가지 유형
발표자는 시스템 내의 비동기 작업을 두 가지로 분류하여 접근했습니다. - 유형 A (장시간 실행 작업): 그룹 생성, 계정 삭제 등 지연을 어느 정도 허용할 수 있는 작업입니다. - 유형 B (준실시간 작업): 이메일 알림, 챗봇 응답, AI 생성 등 비동기임에도 불구하고 사용자 경험을 위해 즉각적인 처리가 필요한 작업입니다. 기존 시스템은 이 두 유형을 효과적으로 분리하여 처리하는 데 한계가 있었으며, Solid Queue 도입을 통해 큐 격리와 우선순위 제어를 실현하고자 했습니다.
3. 왜 Solid Queue를 선택했는가?
팀은 Sidekiq Pro와 같은 대안도 검토했으나, 다음과 같은 이유로 Solid Queue를 최종 선택했습니다.
- 인프라 단순성: Redis와 같은 새로운 구성 요소를 추가하지 않고 기존 Aurora 데이터베이스를 그대로 활용할 수 있습니다. 이는 ‘운영 구성 요소를 불필요하게 늘리지 않는다’는 SRE 원칙에 부합합니다.
- 선형 확장성: FOR UPDATE SKIP LOCKED 구문을 사용하여 워커들이 이미 잠긴 행을 건너뛰고 다른 작업을 즉시 처리할 수 있게 합니다. 이는 워커 수에 비례하여 처리량이 증가하는 선형 확장을 가능하게 합니다.
- Rails 친화적: Rails 코어 팀에서 개발한 공식 백엔드로, Active Job과의 완벽한 통합을 지원하며 향후 유지보수에 유리합니다.
4. 단계별 마이그레이션 전략
대규모 운영 시스템의 안정성을 위해 다음과 같은 5단계 전략을 수행했습니다.
1. 의존성 제거: Delayed Job 전용 API(예: .delay.method)를 제거하고 코드 간의 결합도를 낮췄습니다.
2. Active Job 표준화: 모든 비동기 작업을 ActiveJob::Base를 상속받는 명시적 클래스로 재작성했습니다.
3. Solid Queue 설정: 큐별로 워커와 디스패처를 구성하고, 폴링 간격 및 동시성 제어를 설정했습니다.
4. 점진적 전환: 특정 작업부터 순차적으로 Solid Queue 백엔드로 전환하며 프로덕션 환경의 안정성을 검증했습니다.
5. 기존 시스템 제거: 안정성이 확인된 후 Delayed Job 라이브러리와 관련 데이터를 완전히 삭제했습니다.
5. 관측성(Observability) 및 내부 구조 이해
Solid Queue의 안정적인 운영을 위해 팀은 내부 테이블 구조(Scheduled, Ready, Claimed, Failed)를 심층 분석했습니다. 또한, 다음과 같은 커스텀 메트릭을 구축하여 모니터링 공백을 메웠습니다. - 큐 상태: 대기 중인 작업 수, 예약된 작업, 실행 중인 작업, 재시도 횟수 등을 모니터링합니다. - 성능 지표: 분당 처리량, 최대/중간 대기 시간, 실행 시간 등을 추적하여 지연 발생 시 즉각 대응할 수 있는 체계를 마련했습니다. 이를 통해 시스템의 가시성을 확보하고 장애 대응 능력을 높였습니다.