
무중단 배포인데 진행 중이던 작업이 갑자기 끊기는 이유는 뭘까요? 배포 도구가 새 요청은 막아주지만, 이미 실행 중인 요청이나 잡(job)을 끝까지 기다려주는 시간(drain 타임아웃)이 짧거나 아예 설정돼 있지 않기 때문입니다. 몇 초짜리 API 요청은 이 시간 안에 대부분 끝나지만, 파일 변환이나 리포트 생성처럼 오래 걸리는 작업은 애초에 배포 파이프라인 안에서 처리하면 안 됩니다.
이 글에서는 왜 무중단 배포에서도 진행 중이던 작업이 끊기는지 그 구조를 짚고, drain 시간을 얼마나 잡아야 하는지, 긴 작업을 어떻게 분리해야 하는지를 실제 코드와 함께 정리했습니다.
무중단 배포에서 ‘끊김’이 생기는 정확한 지점
무중단 배포는 트래픽을 라우팅하는 계층(로드밸런서, Nginx, 쿠버네티스 Service)에서 새 요청을 새 버전 인스턴스로 넘기는 방식으로 동작합니다. 문제는 그 다음입니다. 기존 인스턴스에서 이미 처리 중이던 요청, 열려 있던 커넥션, 백그라운드에서 돌던 작업은 라우팅 규칙과 무관하게 그 프로세스 안에서 계속 실행되고 있습니다.
배포 도구는 옛 인스턴스에 종료 신호(SIGTERM)를 보내고, 정해진 유예 시간이 지나면 강제 종료(SIGKILL) 신호를 보냅니다. 쿠버네티스는 이 유예 시간을 terminationGracePeriodSeconds로 설정하며 기본값은 30초입니다. 이 30초 안에 진행 중이던 작업이 끝나지 않으면 그대로 프로세스가 죽고, 작업은 중간에 끊깁니다. 이 동작 방식은 쿠버네티스 공식 문서의 파드 종료 과정 설명에 자세히 나와 있습니다.
AWS ALB를 쓰는 경우도 구조는 비슷합니다. 대상 그룹에서 인스턴스를 제거할 때 기존 연결을 얼마나 기다려줄지 정하는 deregistration delay 값이 기본 300초로 설정돼 있습니다. 이 값이 곧 배포 시 서버 코드 입장에서 확보되는 drain 시간이라고 보시면 됩니다.
drain 타임아웃을 얼마나 잡아야 할까요?

drain 시간은 작업 성격에 따라 필요한 값이 완전히 다릅니다. 아래는 작업 종류별로 어느 정도 접근이 맞는지 정리한 표입니다.
| 작업 유형 | 대략적인 소요 시간 | drain 시간으로 대응 가능한가 |
|---|---|---|
| 일반 API 요청 (조회, 단순 처리) | 수백 ms ~ 2초 | 가능, drain 10~30초로 충분 |
| 파일 업로드·이미지 변환 | 수 초 ~ 수십 초 | 부분 가능, 값을 넉넉히 늘려야 함 |
| 리포트 생성·대량 집계 | 수 분 ~ 수십 분 | 불가능, drain으로 못 버팀 |
| 웹소켓·SSE 실시간 연결 | 연결 지속 시간 자체가 불특정 | 불가능, 별도 종료·재연결 로직 필요 |
표에서 보시듯 drain 시간을 늘리는 방식은 수 초에서 수십 초짜리 작업까지만 유효합니다. drain을 무작정 길게 잡으면 배포 자체가 느려지고, 롤백이 필요한 상황에서도 옛 인스턴스가 오래 떠 있어 리소스를 이중으로 점유하게 되는 트레이드오프가 생깁니다. 즉 drain 시간 조정은 ‘짧은 작업의 끊김’을 줄이는 용도이지, 긴 작업을 안전하게 만드는 방법이 아닙니다.
긴 작업을 배포 파이프라인에서 분리하는 방법
먼저 API 프로세스 쪽은 SIGTERM을 받으면 새 연결을 받지 않고 기존 요청만 마저 처리한 뒤 종료하도록 만들어야 합니다. Node.js/Express 기준 예시는 아래와 같습니다.
const server = app.listen(3000);
process.on('SIGTERM', () => {
console.log('SIGTERM 수신, 새 연결 차단 시작');
server.close(() => {
console.log('진행 중이던 요청 처리 완료, 프로세스 종료');
process.exit(0);
});
// grace period(예: 25초) 안에 못 끝나면 강제 종료
setTimeout(() => {
console.error('타임아웃 발생, 강제 종료');
process.exit(1);
}, 25000);
});
server.close()는 새 연결은 막고 이미 열린 소켓의 응답은 끝까지 처리해 줍니다. 하지만 이 코드는 요청 하나가 수 초~수십 초 걸리는 상황을 가정한 대응이지, 프로세스 안에서 10분짜리 배치 작업을 돌리고 있다면 grace period 안에 끝날 수가 없습니다.
그래서 오래 걸리는 작업은 API 프로세스와 물리적으로 분리해야 합니다. 리포트 생성, 대량 파일 처리 같은 작업은 API 서버가 큐(예: BullMQ, AWS SQS)에 작업을 등록만 하고, 별도의 워커 프로세스가 큐를 소비하며 실제 처리를 담당하는 구조로 바꾸는 방식입니다. 이렇게 하면 API 서버가 배포로 재시작돼도 작업 자체는 큐에 남아 있고, 워커가 재시작된 뒤 이어서 처리할 수 있습니다.
워커 쪽에서 신경 써야 할 부분은 진행 상태를 중간중간 DB나 Redis에 체크포인트로 저장하는 것입니다. 워커가 배포 도중 종료되더라도 다음 워커가 마지막 체크포인트부터 재개할 수 있고, 같은 작업이 중복 실행되더라도 결과가 달라지지 않도록(멱등성) 만들어 두면 재시도 시 데이터가 꼬이지 않습니다.

이 기준으로 배포 전략을 정해 보세요
실제로 어디까지 drain으로 대응하고 어디부터 분리할지 정하려면 아래 순서로 판단해 보시는 걸 권합니다.
- 서비스에서 가장 오래 걸리는 작업의 실제 소요 시간을 로그로 실측해 보세요. 추정치가 아니라 p95, p99 값을 확인하는 게 정확합니다.
- 최대 소요 시간이 drain 가능 범위(대략 수십 초 이내) 안에 들어오면 grace period와 drain 값만 늘려도 충분합니다.
- 수 분 이상 걸리는 작업이 하나라도 있다면, 그 작업은 API 프로세스에서 무조건 큐+워커 구조로 분리해야 합니다.
- 워커도 배포 대상이라면 워커 쪽에도 체크포인트 저장 로직을 넣어야 배포 중 재시작에도 작업이 이어집니다.
다만 큐와 워커를 분리하는 구조는 관리해야 할 컴포넌트가 늘어나고, 인프라 비용과 운영 복잡도가 함께 올라가는 트레이드오프가 있습니다. 트래픽이 적고 배치 작업도 몇 초 안에 끝나는 작은 서비스라면 drain 시간만 넉넉히 잡는 쪽이 오히려 합리적일 수 있습니다. 무조건 큐부터 도입하기보다는 앞선 실측 결과를 보고 결정하시는 게 맞습니다.
drain 시간을 늘렸는데도 파일 업로드가 끊기는 이유는 무엇인가요?
drain 값을 늘렸는데도 여전히 업로드나 요청이 끊긴다면, 원인이 애플리케이션 서버가 아니라 앞단 로드밸런서에 있는 경우가 많습니다. 예를 들어 ALB의 idle timeout은 대상 그룹 deregistration delay와 별개의 값이라서, drain 시간을 늘려도 idle timeout이 짧으면 연결이 그쪽에서 먼저 끊깁니다.
또 다른 흔한 원인은 클라이언트가 이미 새 버전 인스턴스로 라우팅된 상태에서 옛 인스턴스가 처리하던 응답을 기다리는 경우입니다. L7 로드밸런서는 커넥션 단위가 아니라 요청 단위로 라우팅하기 때문에, 같은 클라이언트라도 요청마다 다른 인스턴스로 갈 수 있습니다. 이 경우는 drain 시간 조정이 아니라 세션이나 업로드 상태를 서버 프로세스가 아닌 외부 스토리지(S3, Redis 등)에 두는 구조 변경이 필요합니다. 무중단 배포와 상태 분리(stateless) 설계의 관계는 위키백과의 블루-그린 배포 설명에서도 함께 다루고 있는 부분입니다.
정리하면, 무중단 배포인데 진행 중이던 작업이 끊기는 문제는 drain 시간 하나로 전부 해결되는 게 아닙니다. 짧은 요청은 grace period와 drain 값 조정으로 대응하고, 수 분 이상 걸리는 작업은 API 프로세스에서 큐와 워커로 분리한 뒤 체크포인트로 재개 가능하게 만드는 것이 실제로 끊김을 없애는 구조입니다. 지금 서비스에서 가장 오래 걸리는 작업의 p99 소요 시간부터 로그로 확인해 보시고, 그 값을 기준으로 drain 조정이 맞는지 분리가 맞는지 판단해 보세요.
