👨🍳 우리가 식당을 운영하고 있다고 생각해보자.
손님이 식사를 하고 있고, 주방에서는 요리가 한창이며, 계산대에서는 결제가 이뤄지고 있다.
그런데 어느 날, 갑자기 누군가 “이제 문 닫아야 해요!” 하고 전기를 뚝 끊어버렸다.
- 먹다 말고 나가야 하는 손님들 😨
- 완성되지 않은 요리 🍳
- 계산 중이던 손님 카드 결제 실패 💳
- 심지어 저장되지 않은 장부도 날아감 😭
이렇게 “갑작스러운 종료”는 많은 문제를 일으킬 수 있다.
❗️ Graceful Shutdown은 갑작스러운 종료를 방지하는 방법
"우아한 종료”라는 건, 말 그대로 서비스나 프로그램을 ‘예쁘게’, ‘안전하게’ 종료시키는 과정.
- “곧 종료할게!” 라는 신호를 받으면 👉 서버나 프로그램은 “오! 곧 꺼질 거구나” 하고 준비를 함.
- 새로운 요청을 막음 👉 새로운 손님은 받지 않고, 이미 식당에 들어온 손님만 마무리함.
- 현재 처리 중이던 작업들을 끝까지 마무리
- 👉 파일 저장 중이면 저장을 완료
- 👉 DB 작업 중이면 커밋 or 롤백 처리
- 👉 사용자 요청이면 응답 끝까지 주기
- 모든 걸 안전하게 정리한 뒤 종료
🖥️ 서버에서의 실제 예시
- 서버가 SIGTERM (종료 신호)을 받으면,
- Thread 풀 정리, DB 연결 끊기, Queue 처리 마무리 등을 한 뒤,
- 시스템을 종료
🤔 왜 이렇게까지 해야 할까?
- 데이터 유실 방지
- 사용자 경험 보호 (응답 실패 최소화)
- 자원 누수 방지 (메모리, 커넥션, 파일 등)
- 시스템의 안정성과 신뢰성 확보
🔧 구현 예시 (Java Spring Boot)
// 1) application.properties
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s // 최대 30초 대기
// 2) GracefulShutdownHandler 등록
@Component
public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> {
private final WebServer webServer;
@Autowired
public GracefulShutdown(WebServer webServer) {
this.webServer = webServer;
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("🛑 ContextClosedEvent 수신: 종료 시작");
// 1) 신규 연결 막기
webServer.stop();
// 2) 기존 작업 완료 대기 (spring.lifecycle.timeout 설정 사용)
}
}
// 3) @PreDestroy 로 리소스 정리
@Service
public class SomeService {
@PreDestroy
public void cleanup() {
// 예: DB 커넥션 풀 닫기, 스케줄러 종료
System.out.println("🔧 리소스 정리 중...");
}
}
- server.shutdown=graceful 설정으로 톰캣/언더토우/WebFlux가 SIGTERM 후 자동으로 대기
- ContextClosedEvent로 커스텀 로직 추가 가능
- @PreDestroy 를 활용해 빈 단위로도 정리
🔧 Node.js (Express) 구현 예제
const express = require('express');
const http = require('http');
const app = express();
let server = http.createServer(app);
let connections = new Set();
server.on('connection', (sock) => {
connections.add(sock);
sock.on('close', () => connections.delete(sock));
});
app.get('/', (req, res) => {
// 예: 오래 걸리는 작업
setTimeout(() => res.send('완료!'), 5000);
});
server.listen(3000, () => console.log('🚀 서버 시작'));
function gracefulShutdown() {
console.log('🛑 종료 신호 수신, 신규 연결 차단');
server.close(() => {
console.log('✅ 모든 연결 종료, 프로세스 종료');
process.exit(0);
});
// 아직 닫히지 않은 연결을 강제 종료 (예: 30초 후)
setTimeout(() => {
console.log('⏰ 타임아웃, 강제 종료');
connections.forEach((sock) => sock.destroy());
process.exit(1);
}, 30_000);
}
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
- server.close() 로 더 이상 accept() 하지 않음
- 열린 소켓을 추적해 필요 시 강제 끊기
- 최대 대기 시간을 둬서 무한 대기를 방지
🌟 구현 시 주의사항 & 팁
- 타임아웃: 무한 대기를 막기 위해 적절한 최대 대기시간을 설정
- 상태 저장: 종료 중인 요청에도 “처리 중” 상태를 클라이언트에 알려줄 수 있는 API 설계 고려
- 헬스체크: 로드 밸런서(LB)가 종료 중인 인스턴스를 감지하고 트래픽을 빼도록 /health 엔드포인트 구현
- 테스트: 실제로 kill -TERM <pid> 등을 통해 시그널 동작을 반드시 로컬/스테이징에서 검증
'[배포]' 카테고리의 다른 글
[배포] Nginx 란? (1) | 2025.04.17 |
---|---|
[배포] 🚀 무중단 배포를 위한 3가지 아키텍처 (0) | 2025.04.17 |
[AWS] EC2에 MySQL 설치 및 접속 (0) | 2025.04.05 |
[AWS] 🔐 보안 그룹 설정 (0) | 2025.04.05 |
[GitHub Actions] GitHub Actions 명령어 (0) | 2025.03.14 |