티스토리 뷰

1. 먼저 프로젝트 설명부터

[그림1] 전시회 3면 예시

 

먼저 프로젝트의 메인 기능에 대해 간단히 설명드리겠습니다.

프로젝트에서는 사용자가 이미지 3장을 업로드하면, 이미지에서 감정을 추출한 뒤 감정에 어울리는 AI 배경음을 3개 생성합니다. 이후 생성된 이미지와 배경음을 기반으로 인터랙션 형태의 전시회를 즐길 수 있도록 구성하였습니다.

 

다만 AI 배경음 생성에는 1개당 약 30초, 총 3개 생성 시 약 90초 정도의 시간이 소요되었습니다. 만약 이를 동기 방식으로 처리할 경우, 전시회 조회 및 사용자 인터랙션과 같은 다른 API 요청까지 지연될 가능성이 있다고 판단했습니다. 이에 따라 AI 추론 작업은 Celery 기반 비동기 구조로 분리하여 설계했습니다. 

 

2. 어디가 문제일까?

비동기 구조로 AI 추론 작업을 분리하면서 API 블로킹 문제는 해결할 수 있었지만, 운영 환경에서는 또 다른 문제가 존재했습니다.

만약 작업을 처리하던 Celery Worker가 실행 중 비정상적으로 종료된다면 어떻게 될까요?

 

AI 추론 작업은 수십 초 이상 수행되는 장시간 작업이기 때문에, Worker 장애가 발생할 경우 작업이 중간에 종료된 채 사라질 가능성이 있었습니다. 특히 작업 완료 전에 ACK가 처리되는 구조라면, 실제 작업은 끝나지 않았더라도 Broker 입장에서는 이미 처리 완료된 작업으로 인식할 수 있습니다. 이 경우 사용자는 배경음 생성 결과를 전달받지 못하지만, 시스템 내부에서는 실패 여부를 정확히 인지하지 못하는 문제가 발생할 수 있었습니다. 

 

3. Celery의 ACK 동작 이해하기 및 설정

문제를 해결하기 위해 먼저 Celery가 작업을 어떤 방식으로 처리하는지 확인해보았습니다.

[그림2] acks_late=false

 

Celery는 Broker(Redis)로부터 Task를 가져온 뒤 Worker가 작업을 수행하는 구조로 동작합니다. 이 과정에서 중요한 개념이 바로 ACK입니다. ACK는 Broker에게 “해당 작업을 정상적으로 전달받았다”는 확인 신호를 의미합니다.

 

기본 설정에서는 Worker가 Task를 가져오는 시점에 ACK를 먼저 전달하는 Early ACK 방식으로 동작합니다. 즉 실제 작업이 끝나기 전에 Broker 입장에서는 이미 처리 완료된 작업으로 간주하게 됩니다. 이 구조에서는 Worker가 작업 수행 도중 비정상 종료될 경우 문제가 발생할 수 있습니다. 이미 ACK가 전달된 상태이기 때문에 Broker는 해당 작업을 다시 재전송하지 않으며, 결과적으로 작업이 중간에 유실될 가능성이 존재했습니다.

[그림3] acks_late_true

 

특히 이번 프로젝트의 AI 추론 작업은 평균 30초 이상 수행되는 장시간 작업이었기 때문에, Worker 장애 상황에서 작업 유실 가능성을 반드시 고려해야 했습니다. 이를 해결하기 위해 Task 실행 완료 이후 ACK를 전달하는 Late ACK 방식으로 변경하였습니다.

 

4. acks_late=true로 인한 또 다른 문제...

acks_late=true 설정을 적용하면서 Worker 장애 상황에서도 작업이 다시 Queue에 재전송될 수 있도록 개선할 수 있었습니다.

하지만 문제는 여기서 끝나지 않았습니다. Late ACK 방식에서는 Worker가 작업 수행 도중 비정상 종료되면, Broker 입장에서는 아직 ACK를 받지 못한 상태이므로 해당 작업을 다시 재큐잉합니다. 그런데 이 과정에서 문제가 하나 더 있었습니다. Celery Worker는 기본적으로 여러 개의 Task를 미리 가져와 처리하는 prefetch 방식으로 동작하는데, Worker가 종료되는 시점에 이미 실행 완료됐거나 실행 중이던 다른 task들까지 prefetch된 상태로 함께 일괄 재큐잉되는 문제가 발생했습니다.

즉 죽은 Worker가 가져가 있던 task 묶음 전체가 재전송되면서, 이미 배경음 생성이 끝났거나 진행 중이던 작업까지 중복 실행될 가능성이 생긴 것입니다.

그로 인해 아래와 같은 문제들이 추가적으로 생길 수 있었습니다.

  • 동일한 AI 추론 작업 중복 수행
  • 중복된 결과 저장
  • 불필요한 GPU 리소스 사용
  • 사용자에게 중복 응답 전달 가능성

즉 작업 유실 문제는 해결되었지만, 이번에는 "일괄 재큐잉으로 인한 중복 실행"이라는 새로운 문제가 발생하게 되었습니다. 당시 AI 서버의 리소스가 충분하지 않았기 때문에, 이 중복 실행을 반드시 막아야 했습니다.

 

5. 해결해보자 

이 문제는 두 단계로 나눠서 접근했습니다.

 

첫 번째는 일괄 재큐잉 자체를 막는 것이었습니다. Celery의 worker_prefetch_multiplier를 1로 변경해, 워커 하나가 한 번에 1개의 task만 가져가도록 제한했습니다. 이를 통해 Worker 장애 시 함께 딸려있던 다른 task들까지 무더기로 재큐잉되는 문제를 방지할 수 있었습니다.

 

두 번째는 그럼에도 남아있는 재실행 가능성에 대한 방어 장치였습니다. "동일한 Task가 다시 실행되더라도 결과가 중복 생성되지 않도록 만들 수 있을까?"를 고민했고, task 진입 시점에 먼저 DB에서 현재 상태를 조회하도록 구조를 개선했습니다. 이미 COMPLETED 상태라면 AI 추론 호출 자체를 건너뛰고 기존 결과를 반환하도록 했고, 아직 처리되지 않은 작업인 경우에만 실제 AI 추론을 수행하도록 설계했습니다.

 

6. 최종 비동기 구조 정리 

[그림 4] Celery + Redis 비동기 처리 구조

 

최종적으로는 단순히 AI 추론 작업을 비동기로 분리하는 것에서 끝나지 않고, Worker 장애 상황에서도 작업 유실·중복 없이 안정적으로 복구 가능한 구조를 목표로 개선했습니다.

이를 위해 다음과 같은 운영 기준을 적용했습니다.

  • task_acks_late=True: 작업 완료 이후 ACK 수행 → Worker crash 시 재큐잉 보장
  • task_reject_on_worker_lost=True: 워커 유실 시 작업 반환
  • worker_prefetch_multiplier=1: 워커당 1개 task만 처리 → 일괄 재큐잉으로 인한 중복 방지
  • task 진입 시 DB 상태 조회 기반 idempotency 가드: COMPLETED 상태면 AI 호출 스킵

최종적으로 Worker 장애 상황에서도 작업 유실 없이 재처리가 가능해졌으며, 동일 Task가 여러 번 재큐잉되더라도 idempotency 가드를 통해 AI 서버 호출이 정확히 1회로 보장되고, 재시도로 인한 부하가 누적되지 않는 구조를 구성할 수 있었습니다.