01 · OVERVIEW

개요

gif lower는 사용자가 업로드한 GIF 파일을 FFmpeg로 변환해 용량을 줄인 결과물을 제공하는 서비스입니다. 변환 기능 자체는 정상적으로 동작했지만, 여러 요청이 동시에 들어오는 상황에서는 FFmpeg 프로세스가 한꺼번에 실행될 수 있는 구조였습니다.

02 · PROBLEM

문제 상황

각 변환 요청은 CPU를 많이 사용하는 FFmpeg 작업을 생성합니다. 요청 하나만 처리할 때는 문제가 없었지만, 여러 사용자의 요청이 겹치면 실행 중인 프로세스가 증가하고 API 서버까지 영향을 받을 가능성이 있었습니다.

03 · IMPACT

영향 범위

변환 자체의 실패보다 더 큰 위험은 같은 Node.js 서비스가 제공하는 작업 생성·상태 조회·다운로드·헬스체크 응답까지 느려지는 것이었습니다. 따라서 목표를 “FFmpeg 성공”이 아니라 “변환 요청이 겹쳐도 API 제어 흐름을 유지”하는 것으로 잡았습니다.

영향 범위: 단일 Linux 서버의 CPU와 메모리, 같은 프로세스가 제공하는 HTTP API. 데이터 손상이나 외부 장애는 확인되지 않았습니다.
04 · EVIDENCE

확인된 증거

  • GIF 후보를 만들 때마다 별도 ffmpeg 자식 프로세스를 실행합니다.
  • 한 작업도 여러 품질 프리셋을 순서대로 시험할 수 있습니다.
  • 작업 생성 API는 완료를 기다리지 않고 202 Accepted와 작업 ID를 반환합니다.
  • 현재 소스는 pendingJobs, activeJobs, MAX_CONCURRENT_JOBS로 실행 수를 통제합니다.
근거: 공개 저장소의 src/server.js, src/compressor.js, 커밋 0eddbc9·2fd5bdf. 과거 CPU 퍼센트와 응답시간 원본은 남아 있지 않습니다.
05 · HYPOTHESIS

가설을 나눴습니다

  • 업로드 파일 크기 때문에 전송 구간이 느린가
  • FFmpeg 한 작업 자체의 프리셋 탐색이 오래 걸리는가
  • 여러 작업이 동시에 자원을 경합하는가
  • 완료 파일과 임시 파일이 누적돼 디스크까지 압박하는가

단일 작업 최적화와 동시 요청 제어는 별개 문제로 보고, 먼저 서버 전체에 영향을 주는 동시성 경계를 해결했습니다.

06 · DIAGNOSIS

진단 순서

  1. 요청 한 건이 생성하는 자식 프로세스 흐름을 확인했습니다.
  2. 후보 프리셋 반복이 한 작업의 실행 시간을 늘릴 수 있음을 확인했습니다.
  3. 요청 수가 그대로 동시 실행 수가 되는 구조인지 점검했습니다.
  4. HTTP 응답과 실제 변환 실행을 분리할 수 있는지 검토했습니다.
판단 기준: “요청 접수 수”와 “현재 실행 수”가 분리되어야 하며, 실행 수는 환경변수의 상한을 넘지 않아야 합니다.
07 · ROOT CAUSE

실제 원인

FFmpeg 자체의 오류가 아니라 CPU 집약적인 작업에 동시성 경계가 없던 실행 구조가 원인이었습니다. HTTP 요청이 들어오는 속도와 서버가 변환을 처리할 수 있는 속도가 다르므로, 둘 사이에 대기열과 실행 상한이 필요했습니다.

08 · DECISION

대안을 비교했습니다

업로드 크기 제한한 작업의 상한은 만들지만 동시 실행 수는 제어하지 못함
초과 요청 거부서버는 보호하지만 사용자가 작업을 잃음
프로세스 우선순위 조정경합의 강도만 낮추고 개수는 그대로임
메모리 큐 + 동시성 제한현재 단일 서버 규모에서 가장 단순하게 실행 상한을 보장
09 · ACTION

선택한 조치

작업 생성 요청에는 즉시 작업 ID를 반환하고, 실제 변환은 pendingJobs에 넣었습니다. runNextJob()은 실행 중 작업이 설정값보다 적을 때만 다음 작업을 시작합니다.

const maxConcurrentJobs = Math.max(
  1,
  Number(process.env.MAX_CONCURRENT_JOBS) || 2
);

function runNextJob() {
  while (activeJobs < maxConcurrentJobs && pendingJobs.length > 0) {
    const next = pendingJobs.shift();
    activeJobs += 1;
    void processJob(next.job, next.targetBytes);
  }
}
10 · IMPLEMENTATION

큐만 넣고 끝내지 않았습니다

  • /health에서 실행·대기 작업 수를 확인할 수 있습니다.
  • IP별 작업 생성은 한 시간에 12개로 제한했습니다.
  • 업로드는 GIF 한 개, 최대 100MB로 제한했습니다.
  • 완료·실패 작업과 임시 파일은 15분 뒤 정리합니다.
  • finally에서 실행 수를 감소시켜 실패해도 큐가 계속 진행됩니다.
11 · VERIFICATION

검증과 결과

  • node --test 실행 결과 4개 테스트가 모두 통과했습니다.
  • FPS 파싱, 품질 프리셋 순서, 크기 표시, 해상도 프리셋을 검증했습니다.
  • 대기 작업과 실행 작업이 별도 상태로 관리됩니다.
  • 실행 중 작업 수는 MAX_CONCURRENT_JOBS의 상한 안에서만 증가합니다.
2026-08-28 재검증: tests 4 · pass 4 · fail 0. 공개 소스와 현재 포트폴리오의 설명도 대조했습니다.
12 · OPERATIONS

운영 안전장치

동시성 상한, 요청 제한, 업로드 제한, 실패 처리, 파일 보존 시간을 함께 두었습니다. CPU 집약 작업을 추가할 때도 “접수 → 대기 → 실행 → 완료/실패 → 정리”의 수명주기를 먼저 설계하도록 기준을 남겼습니다.

13 · LIMITS & NEXT

기록 한계와 다음 단계

  • 장애 당시 CPU·메모리·응답시간 시계열은 남아 있지 않아 수치를 만들지 않았습니다.
  • 현재 큐는 단일 서버 메모리에 있어 재시작하면 대기 작업을 잃을 수 있습니다.
  • 다중 인스턴스로 확장할 때는 Redis 기반 큐와 별도 Worker가 필요합니다.
  • 다음 증거는 동시 요청 부하 테스트와 p95 응답시간 비교입니다.
14 · LEARNING

배운 점

기능이 정상적으로 실행되는 것과 운영 환경에서 안전하게 실행되는 것은 다른 문제였습니다. CPU 집약 작업은 요청 수를 그대로 프로세스 수로 바꾸지 말고, 서버 용량에 맞는 명시적인 실행 경계를 가져야 합니다.