Contents
see List왜 종료 처리까지 설계해야 하는가
Node.js API 서버는 새 버전을 배포하거나 컨테이너를 재시작할 때 SIGTERM 신호를 받습니다. 이때 프로세스를 즉시 종료하면 처리 중이던 HTTP 요청, 데이터베이스 트랜잭션, 메시지 발행 작업이 중간에 끊길 수 있습니다. 사용자는 502 또는 네트워크 오류를 보게 되고, 결제·주문·파일 처리처럼 재시도가 어려운 작업에서는 데이터 불일치도 생길 수 있습니다.
Graceful Shutdown은 종료 신호를 받은 뒤 새 요청은 더 받지 않고, 진행 중인 요청을 제한 시간 안에 마무리한 다음 연결과 리소스를 닫는 절차입니다. 로드밸런서, Kubernetes, Docker 환경에서는 애플리케이션이 이 절차를 지켜야 배포 시 오류율이 급증하지 않습니다. 핵심은 종료 자체를 빠르게 만드는 것이 아니라, 새 유입을 차단하고 이미 시작한 작업의 종료 경계를 명확히 하는 데 있습니다.
종료 흐름을 먼저 정리하기
- SIGTERM 또는 SIGINT를 한 번만 처리하고 종료 모드로 전환합니다.
- readiness 상태를 실패로 바꿔 로드밸런서가 새 요청을 보내지 않게 합니다.
- HTTP 서버의 listen 소켓을 닫아 새 연결 수락을 중지합니다.
- 진행 중인 요청과 백그라운드 작업이 끝날 시간을 제한합니다.
- DB pool, Redis, 메시지 브로커 등 외부 연결을 순서대로 정리합니다.
- 제한 시간을 넘기면 오류 로그를 남기고 비정상 종료하여 무한 대기를 막습니다.
특히 readiness와 liveness를 같은 의미로 쓰면 안 됩니다. 종료 중인 프로세스는 살아 있으므로 liveness는 정상일 수 있지만, 새 요청을 받아서는 안 되므로 readiness는 즉시 실패해야 합니다. 운영 환경에서 헬스체크 URL 하나로 두 상태를 같이 표현한다면 배포 중 트래픽이 계속 유입되는 문제가 생길 수 있습니다.
Express 서버에 적용하는 기본 구현
아래 예제는 현재 요청 수를 추적하고, SIGTERM 수신 후 /ready 요청에는 503을 반환합니다. server.close()는 새 연결 수락을 중단하고 기존 연결이 종료될 때까지 기다립니다. 실제 서비스에서는 데이터베이스와 캐시 클라이언트의 종료 함수를 추가해야 합니다.
import express from 'express';
import http from 'node:http';
const app = express();
let shuttingDown = false;
let activeRequests = 0;
app.use((req, res, next) => {
if (shuttingDown && req.path !== '/live') {
return res.status(503).json({ message: 'server is shutting down' });
}
activeRequests += 1;
res.once('finish', () => { activeRequests -= 1; });
next();
});
app.get('/live', (req, res) => res.sendStatus(200));
app.get('/ready', (req, res) => {
res.sendStatus(shuttingDown ? 503 : 200);
});
app.get('/api/report', async (req, res) => {
const report = await makeReport();
res.json(report);
});
const server = http.createServer(app);
server.listen(3000);
async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
console.log(signal + ' received; active=' + activeRequests);
const forceExit = setTimeout(() => {
console.error('shutdown timeout exceeded');
process.exit(1);
}, 25_000);
forceExit.unref();
server.close(async (error) => {
try {
if (error) throw error;
await closeDatabasePool();
await closeRedisClient();
process.exit(0);
} catch (err) {
console.error('shutdown failed', err);
process.exit(1);
}
});
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));긴 요청과 Keep-Alive 연결의 함정
server.close()만 호출했는데 종료가 오래 걸리는 경우가 있습니다. HTTP Keep-Alive 연결이 살아 있거나, 응답을 끝내지 못한 스트리밍 요청이 남아 있기 때문입니다. Node.js 서버의 requestTimeout, headersTimeout, keepAliveTimeout을 서비스 특성에 맞게 설정하고, 긴 다운로드·SSE·WebSocket은 별도 종료 정책을 둬야 합니다. 배포 대기 시간보다 긴 타임아웃을 두면 종료 제한 시간이 사실상 의미 없어집니다.
WebSocket을 사용하는 서비스는 연결 목록을 관리한 뒤 종료 단계에서 close 코드를 보내고 짧은 유예 시간을 둡니다. 작업 큐 consumer도 새 메시지 수신을 먼저 중지한 뒤, 이미 받은 메시지의 ack 또는 재시도 처리를 마쳐야 합니다. 반대로 cron 성격의 긴 배치 작업은 HTTP 서버와 같은 프로세스에 묶지 않는 편이 종료 제어와 장애 격리에 유리합니다.
컨테이너와 오케스트레이터 설정
Docker와 Kubernetes는 보통 SIGTERM을 보내고 지정된 유예 시간 뒤 SIGKILL을 보냅니다. 애플리케이션의 강제 종료 타이머는 이 유예 시간보다 짧아야 합니다. 예를 들어 Kubernetes의 terminationGracePeriodSeconds가 30초라면 앱 내부 정리는 25초 안에 끝내도록 설계합니다. readiness probe가 503으로 바뀐 직후에도 프록시와 엔드포인트 반영에는 짧은 지연이 있을 수 있으므로, 서버를 닫기 전에 수 초의 drain 시간을 두는 전략도 트래픽 패턴에 따라 검토할 수 있습니다.
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: example/api:20260727
readinessProbe:
httpGet:
path: /ready
port: 3000
periodSeconds: 5
livenessProbe:
httpGet:
path: /live
port: 3000
periodSeconds: 10배포 전 체크리스트
- 종료 신호를 받은 뒤 새 요청이 503 또는 로드밸런서 우회로 처리되는지 확인합니다.
- 처리 중인 요청, DB 트랜잭션, 메시지 소비 작업이 유예 시간 내에 끝나는지 부하 환경에서 검증합니다.
- 강제 종료 발생 횟수와 종료 시작·완료 시간을 로그와 지표로 남깁니다.
- readiness와 liveness의 의미를 분리하고 배포 유예 시간과 앱 타임아웃을 일치시킵니다.
- WebSocket, 스트림, 작업 큐처럼 일반 HTTP 종료로 해결되지 않는 연결의 정리 정책을 문서화합니다.
Graceful Shutdown은 코드 몇 줄을 추가하는 작업이 아니라, 요청 수명·외부 연결·배포 제한 시간을 하나의 종료 계약으로 맞추는 일입니다. 이 계약을 테스트 환경에서 재현해 두면 배포와 장애 복구 시 예측 가능한 서비스 동작을 만들 수 있습니다.
javascript
| No | 작성일 | Title |
|---|---|---|
| 1076 | 2016. 09. 09. | [ AngularJS ] promise then 사용 |
| 1075 | 2016. 09. 07. | [ AngularJS ] 형제 태그 ng-repeat |
| 1074 | 2016. 09. 07. | [ AngularJS ] 의 이벤트 전파 |
| 1041 | 2016. 08. 18. | [ AngularJS ] json parsing spring ajax (and $$hashKey error ) |
| 1040 | 2016. 08. 18. | [ AngularJS ] select option ng-options |
| 1039 | 2016. 08. 18. | [ AngularJS ] ng-repeat 에서 ng-model 사용시 제대로 작동안할때 |
| 1038 | 2016. 08. 16. | [ AngularJS ] 어느 위치에서나 scope 에 접근하기 (전역,지역) |
| 360 | 2016. 05. 31. | [ bootstrap ] bootstrap modal multi modal wheel and focus not work |
| 359 | 2016. 05. 03. | [ javascript ] 이벤트 전파 |
| 357 | 2016. 04. 16. | [ datatable ] jquery datatable and editor exemple 사용법 예 |