From 2fc61a50c8f942f75bf6799371139cf5942f2271 Mon Sep 17 00:00:00 2001 From: Nuung Date: Sat, 2 Aug 2025 00:11:10 +0900 Subject: [PATCH 1/7] =?UTF-8?q?modify:=20db=20config=20update,=20pool=20?= =?UTF-8?q?=EC=A2=80=20=EB=8D=94=20=EC=84=B8=EB=B0=80=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/configs/db.config.ts | 59 ++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/src/configs/db.config.ts b/src/configs/db.config.ts index 6a6e954..e4ec966 100644 --- a/src/configs/db.config.ts +++ b/src/configs/db.config.ts @@ -16,7 +16,8 @@ const poolConfig: pg.PoolConfig = { port: Number(process.env.POSTGRES_PORT), max: 10, // 최대 연결 수 idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초) - connectionTimeoutMillis: 5000, // 연결 시간 초과 (5초) + connectionTimeoutMillis: 10000, // 연결 시간 초과 (10초) + }; if (process.env.NODE_ENV === 'production') { @@ -27,15 +28,49 @@ if (process.env.NODE_ENV === 'production') { const pool = new Pool(poolConfig); -(async () => { - const client = await pool.connect(); - try { - await client.query('CREATE EXTENSION IF NOT EXISTS timescaledb;'); - logger.info('TimescaleDB 확장 성공'); - } catch (error) { - logger.error('TimescaleDB 초기화 실패 : ', error); - } finally { - client.release(); +async function initializeDatabase(): Promise { + const maxRetries = 3; + let delay = 800; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + logger.info(`데이터베이스 연결 시도 ${attempt}/${maxRetries}`); + + const client = await pool.connect(); + + try { + // 연결 테스트 + await client.query('SELECT 1'); + logger.info('데이터베이스 연결 성공'); + + // TimescaleDB 확장 (필수) + await client.query('CREATE EXTENSION IF NOT EXISTS timescaledb;'); + logger.info('TimescaleDB 확장 성공'); + + return; // 성공 + + } finally { + client.release(); + } + + } catch (error) { + logger.error(`데이터베이스 연결 실패 (시도 ${attempt}/${maxRetries}):`, error); + + if (attempt === maxRetries) { + logger.error('데이터베이스 연결에 완전히 실패했습니다. 서버를 종료합니다.'); + process.exit(1); // 연결 실패시 서버 종료 + } + + logger.info(`${delay}ms 후 재시도...`); + await new Promise(resolve => setTimeout(resolve, delay)); + delay = Math.floor(delay * 1.5); + } } -})(); -export default pool; +} + +initializeDatabase().catch(error => { + logger.error('데이터베이스 초기화 중 예상치 못한 오류:', error); + process.exit(1); // 치명적 오류시 서버 종료 +}); + +export default pool; \ No newline at end of file From caf66d65562d785a64ec438714221111a058fb8d Mon Sep 17 00:00:00 2001 From: Nuung Date: Sat, 2 Aug 2025 01:03:58 +0900 Subject: [PATCH 2/7] =?UTF-8?q?hotfix:=20run=20script=20api=20image=20?= =?UTF-8?q?=EC=9E=98=20=EC=82=AC=EC=9A=A9=ED=95=98=EA=B2=8C=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run.sh | 74 +++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/run.sh b/run.sh index 7425a23..f97c469 100755 --- a/run.sh +++ b/run.sh @@ -33,11 +33,12 @@ check_docker() { # 서비스 중지 stop_services() { - print_step "0. 현재 사용 중 이미지 stop, down" + print_step "0. 현재 사용 중인 서비스 중지" docker compose stop || true docker compose down || true } +# Docker 리소스 정리 cleanup_docker() { print_step "0.5. 사용하지 않는 Docker 리소스 정리" @@ -53,37 +54,82 @@ cleanup_docker() { echo -e "${GREEN}Docker 정리 완료${NC}" } -# 이미지 업데이트 +# 모든 이미지 업데이트 (API 포함) update_images() { - print_step "1. 외부 이미지 업데이트 (fe, nginx)..." - docker compose pull fe nginx -} - -# API 빌드 -build_api() { - print_step "2. 로컬 이미지 빌드 (api)..." - docker compose build api + print_step "1. Docker Hub에서 최신 이미지 다운로드 중..." + + # 모든 서비스의 이미지를 Docker Hub에서 최신 버전으로 pull + docker compose pull + + echo -e "${GREEN}모든 이미지 업데이트 완료${NC}" } # 서비스 시작 start_services() { - print_step "3. 서비스 재시작..." + print_step "2. 서비스 시작 중..." docker compose up -d + + echo -e "${GREEN}모든 서비스가 시작되었습니다${NC}" +} + +# 서비스 상태 확인 +check_services() { + print_step "3. 서비스 상태 확인" + + # 잠시 대기 (서비스 시작 시간 확보) + sleep 5 + + # 실행 중인 컨테이너 확인 + echo -e "${YELLOW}실행 중인 컨테이너:${NC}" + docker compose ps + + # 각 서비스 헬스체크 + echo -e "\n${YELLOW}서비스 헬스체크:${NC}" + + # API 서비스 확인 + if curl -f http://localhost:8080/health &>/dev/null; then + echo -e "✅ API 서비스: ${GREEN}정상${NC}" + else + echo -e "❌ API 서비스: ${RED}응답 없음${NC}" + fi + + # Frontend 서비스 확인 (포트 3000) + if curl -f http://localhost:3000 &>/dev/null; then + echo -e "✅ Frontend 서비스: ${GREEN}정상${NC}" + else + echo -e "❌ Frontend 서비스: ${RED}응답 없음${NC}" + fi + + # Nginx 서비스 확인 (포트 80) + if curl -f http://localhost &>/dev/null; then + echo -e "✅ Nginx 서비스: ${GREEN}정상${NC}" + else + echo -e "❌ Nginx 서비스: ${RED}응답 없음${NC}" + fi } # 메인 실행 로직 main() { set -e # 스크립트 실행 중 오류 발생 시 종료 + print_step "Velog Dashboard V2 배포 스크립트 시작" + check_docker stop_services cleanup_docker update_images - build_api start_services + check_services - print_step "모든 작업이 완료되었습니다! 로그 모니터링을 시작합니다." - sleep 1 + print_step "🎉 모든 작업이 완료되었습니다!" + echo -e "${GREEN}서비스 접속 정보:${NC}" + echo -e "• 메인 사이트: ${YELLOW}http://localhost${NC}" + echo -e "• API 서버: ${YELLOW}http://localhost:8080${NC}" + echo -e "• Frontend: ${YELLOW}http://localhost:3000${NC}" + echo -e "• API Health Check: ${YELLOW}http://localhost:8080/health${NC}" + + echo -e "\n${YELLOW}로그 모니터링을 시작합니다... (Ctrl+C로 종료)${NC}" + sleep 2 docker compose logs -f } From 86d56f0875c9cdd760a210ec3c66ce70bbc49f26 Mon Sep 17 00:00:00 2001 From: Nuung Date: Sat, 2 Aug 2025 01:25:29 +0900 Subject: [PATCH 3/7] modify: db config linting --- src/configs/db.config.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/configs/db.config.ts b/src/configs/db.config.ts index e4ec966..6067399 100644 --- a/src/configs/db.config.ts +++ b/src/configs/db.config.ts @@ -17,7 +17,6 @@ const poolConfig: pg.PoolConfig = { max: 10, // 최대 연결 수 idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초) connectionTimeoutMillis: 10000, // 연결 시간 초과 (10초) - }; if (process.env.NODE_ENV === 'production') { @@ -48,11 +47,9 @@ async function initializeDatabase(): Promise { logger.info('TimescaleDB 확장 성공'); return; // 성공 - } finally { client.release(); } - } catch (error) { logger.error(`데이터베이스 연결 실패 (시도 ${attempt}/${maxRetries}):`, error); @@ -62,15 +59,15 @@ async function initializeDatabase(): Promise { } logger.info(`${delay}ms 후 재시도...`); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); delay = Math.floor(delay * 1.5); } } } -initializeDatabase().catch(error => { +initializeDatabase().catch((error) => { logger.error('데이터베이스 초기화 중 예상치 못한 오류:', error); process.exit(1); // 치명적 오류시 서버 종료 }); -export default pool; \ No newline at end of file +export default pool; From cabe3fa94e72fc4cf3c42775c8a3d691ed6ea19b Mon Sep 17 00:00:00 2001 From: Nuung Date: Sat, 2 Aug 2025 01:41:30 +0900 Subject: [PATCH 4/7] =?UTF-8?q?modify:=20app=20=EC=8B=A4=ED=96=89=EA=B3=BC?= =?UTF-8?q?=20index=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8,=20=EA=B7=B8?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20dbms=20connection=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/configs/db.config.ts | 29 +++++++++-- src/index.ts | 102 ++++++++++++++++++++++++++++----------- 2 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/configs/db.config.ts b/src/configs/db.config.ts index 6067399..db24bf0 100644 --- a/src/configs/db.config.ts +++ b/src/configs/db.config.ts @@ -27,7 +27,14 @@ if (process.env.NODE_ENV === 'production') { const pool = new Pool(poolConfig); -async function initializeDatabase(): Promise { +/** + * 데이터베이스 연결을 초기화하고 TimescaleDB 확장을 보장 + * 최대 3회 재시도하며, 실패 시 서버를 종료 + * + * @throws 연결에 3회 실패하면 서버가 종료 + * @returns {Promise} 연결 및 확장 완료 시 resolve되는 프로미스 + */ +export async function initializeDatabase(): Promise { const maxRetries = 3; let delay = 800; @@ -65,9 +72,21 @@ async function initializeDatabase(): Promise { } } -initializeDatabase().catch((error) => { - logger.error('데이터베이스 초기화 중 예상치 못한 오류:', error); - process.exit(1); // 치명적 오류시 서버 종료 -}); + +/** + * 데이터베이스 커넥션 풀을 안정적으로 직접 종료하는 함수 + * + * @throws 데이터베이스 연결 종료에 실패할 경우 에러 + * @returns {Promise} 데이터베이스 연결이 종료되면 resolve되는 프로미스 + */ +export async function closeDatabase(): Promise { + try { + await pool.end(); + logger.info('데이터베이스 연결이 정상적으로 종료되었습니다.'); + } catch (error) { + logger.error('데이터베이스 연결 종료 중 오류 발생:', error); + throw error; + } +} export default pool; diff --git a/src/index.ts b/src/index.ts index 44a2252..59603fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,45 +1,91 @@ import app from '@/app'; import logger from '@/configs/logger.config'; import { closeCache } from './configs/cache.config'; +import { closeDatabase, initializeDatabase } from './configs/db.config'; +import { Server } from 'http'; const port = parseInt(process.env.PORT || '8080', 10); -const server = app.listen(port, () => { - logger.info(`Server running on port ${port}`); - logger.info(`Environment: ${process.env.NODE_ENV}`); - if (process.env.NODE_ENV !== 'production') { - logger.info(`API Docs: http://localhost:${port}/api-docs`); + +async function startServer() { + try { + // 데이터베이스 초기화 + await initializeDatabase(); + logger.info('데이터베이스 초기화 완료'); + + // 서버 시작 + const server = app.listen(port, () => { + logger.info(`Server running on port ${port}`); + logger.info(`Environment: ${process.env.NODE_ENV}`); + if (process.env.NODE_ENV !== 'production') { + logger.info(`API Docs: http://localhost:${port}/api-docs`); + } + logger.info(`Health Check: http://localhost:${port}/health`); + }); + + // Graceful shutdown 핸들러 설정 + setupGracefulShutdown(server); + + } catch (error) { + logger.error('서버 시작 중 오류 발생:', error); + process.exit(1); } - logger.info(`Health Check: http://localhost:${port}/health`); -}); +} + +function setupGracefulShutdown(server: Server) { + // 기본적인 graceful shutdown 추가 + const gracefulShutdown = async (signal: string) => { + logger.info(`${signal} received, shutting down gracefully`); + + try { + // HTTP 서버 종료 + await new Promise((resolve) => { + server.close(() => { + logger.info('HTTP server closed'); + resolve(); + }); + }); -// 기본적인 graceful shutdown 추가 -const gracefulShutdown = async (signal: string) => { - logger.info(`${signal} received, shutting down gracefully`); - await closeCache(); + // 데이터베이스 연결 종료 + await closeDatabase(); - server.close(() => { - logger.info('HTTP server closed'); - process.exit(0); + // 캐시 연결 종료 + await closeCache(); + + logger.info('Graceful shutdown completed'); + process.exit(0); + } catch (error) { + logger.error('Graceful shutdown 중 오류 발생:', error); + process.exit(1); + } + }; + + // 시그널 핸들러 등록 + process.on('SIGTERM', async () => gracefulShutdown('SIGTERM')); + process.on('SIGINT', async () => gracefulShutdown('SIGINT')); + + // 예상치 못한 에러 처리 + process.on('uncaughtException', async (error) => { + logger.error('Uncaught Exception:', error); + await gracefulShutdown('UNCAUGHT_EXCEPTION'); + }); + + process.on('unhandledRejection', async (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); + await gracefulShutdown('UNHANDLED_REJECTION'); }); // 강제 종료 타이머 (10초) - setTimeout(() => { + const forceExitTimer = setTimeout(() => { logger.error('Could not close connections in time, forcefully shutting down'); process.exit(1); }, 10000); -}; - -process.on('SIGTERM', async () => gracefulShutdown('SIGTERM')); -process.on('SIGINT', async () => gracefulShutdown('SIGINT')); -// 예상치 못한 에러 처리 -process.on('uncaughtException', async (error) => { - logger.error('Uncaught Exception:', error); - await gracefulShutdown('UNCAUGHT_EXCEPTION'); -}); + // graceful shutdown이 완료되면 타이머 해제 + process.on('exit', () => { + clearTimeout(forceExitTimer); + }); +} -process.on('unhandledRejection', async (reason, promise) => { - logger.error('Unhandled Rejection at:', promise, 'reason:', reason); - await gracefulShutdown('UNHANDLED_REJECTION'); -}); +// 서버 시작 +startServer(); \ No newline at end of file From 919660f7e5aa217c697d1b20b034f4b94e5ea9ca Mon Sep 17 00:00:00 2001 From: Nuung Date: Sat, 2 Aug 2025 01:43:03 +0900 Subject: [PATCH 5/7] modify: linting --- src/configs/db.config.ts | 1 - src/index.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/configs/db.config.ts b/src/configs/db.config.ts index db24bf0..c323f73 100644 --- a/src/configs/db.config.ts +++ b/src/configs/db.config.ts @@ -72,7 +72,6 @@ export async function initializeDatabase(): Promise { } } - /** * 데이터베이스 커넥션 풀을 안정적으로 직접 종료하는 함수 * diff --git a/src/index.ts b/src/index.ts index 59603fc..350f776 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ import { Server } from 'http'; const port = parseInt(process.env.PORT || '8080', 10); - async function startServer() { try { // 데이터베이스 초기화 @@ -25,7 +24,6 @@ async function startServer() { // Graceful shutdown 핸들러 설정 setupGracefulShutdown(server); - } catch (error) { logger.error('서버 시작 중 오류 발생:', error); process.exit(1); @@ -88,4 +86,4 @@ function setupGracefulShutdown(server: Server) { } // 서버 시작 -startServer(); \ No newline at end of file +startServer(); From 0a83c21947c91b9952366018d266196c6b497f2b Mon Sep 17 00:00:00 2001 From: Nuung Date: Sat, 2 Aug 2025 11:30:31 +0900 Subject: [PATCH 6/7] =?UTF-8?q?modify:=20index=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=ED=99=94,=20=EA=B3=A0=EB=8F=84=ED=99=94,=20?= =?UTF-8?q?=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 7 +- src/index.ts | 218 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 161 insertions(+), 64 deletions(-) diff --git a/src/app.ts b/src/app.ts index d3142bb..eb0c4cb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,15 +11,12 @@ import router from '@/routes'; import { NotFoundError } from '@/exception'; import { options } from '@/configs/swagger.config'; -import { initSentry, getSentryStatus } from '@/configs/sentry.config'; -import { initCache, getCacheStatus } from '@/configs/cache.config'; +import { getSentryStatus } from '@/configs/sentry.config'; +import { getCacheStatus } from '@/configs/cache.config'; import { errorHandlingMiddleware } from '@/middlewares/errorHandling.middleware'; dotenv.config(); -initSentry(); // Sentry 초기화 -initCache(); // Redis 캐시 초기화 - const app: Application = express(); // 실제 클라이언트 IP를 알기 위한 trust proxy 설정 diff --git a/src/index.ts b/src/index.ts index 350f776..3705e07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,44 +1,43 @@ import app from '@/app'; import logger from '@/configs/logger.config'; -import { closeCache } from './configs/cache.config'; +import { closeCache, initCache } from './configs/cache.config'; import { closeDatabase, initializeDatabase } from './configs/db.config'; import { Server } from 'http'; +import { initSentry } from './configs/sentry.config'; -const port = parseInt(process.env.PORT || '8080', 10); +interface ShutdownHandler { + cleanup(): Promise; +} -async function startServer() { - try { - // 데이터베이스 초기화 - await initializeDatabase(); - logger.info('데이터베이스 초기화 완료'); +/** + * 서버의 graceful shutdown을 담당하는 매니저 클래스 + */ +class GracefulShutdownManager implements ShutdownHandler { + private isShuttingDown = false; + private readonly shutdownTimeout = 10000; // 10초 강제 종료 타이머 - // 서버 시작 - const server = app.listen(port, () => { - logger.info(`Server running on port ${port}`); - logger.info(`Environment: ${process.env.NODE_ENV}`); - if (process.env.NODE_ENV !== 'production') { - logger.info(`API Docs: http://localhost:${port}/api-docs`); - } - logger.info(`Health Check: http://localhost:${port}/health`); - }); + constructor(private server: Server) { } - // Graceful shutdown 핸들러 설정 - setupGracefulShutdown(server); - } catch (error) { - logger.error('서버 시작 중 오류 발생:', error); - process.exit(1); - } -} + /** + * 모든 연결을 안전하게 정리하고 서버를 종료 + */ + async cleanup(): Promise { + if (this.isShuttingDown) { + return; + } -function setupGracefulShutdown(server: Server) { - // 기본적인 graceful shutdown 추가 - const gracefulShutdown = async (signal: string) => { - logger.info(`${signal} received, shutting down gracefully`); + this.isShuttingDown = true; + + // 강제 종료 타이머 설정 (데드락 방지) + const forceExitTimer = setTimeout(() => { + logger.error('Could not close connections in time, forcefully shutting down'); + process.exit(1); + }, this.shutdownTimeout); try { // HTTP 서버 종료 await new Promise((resolve) => { - server.close(() => { + this.server.close(() => { logger.info('HTTP server closed'); resolve(); }); @@ -46,44 +45,145 @@ function setupGracefulShutdown(server: Server) { // 데이터베이스 연결 종료 await closeDatabase(); + logger.info('Database connections closed'); // 캐시 연결 종료 await closeCache(); + logger.info('Cache connections closed'); + clearTimeout(forceExitTimer); // 정상 종료 시 강제 타이머 해제 logger.info('Graceful shutdown completed'); - process.exit(0); } catch (error) { - logger.error('Graceful shutdown 중 오류 발생:', error); + clearTimeout(forceExitTimer); + throw error; + } + } + + /** + * 시그널을 받아 graceful shutdown을 시작 + */ + handleShutdown(signal: string): void { + if (this.isShuttingDown) { + logger.info(`Already shutting down, ignoring ${signal}`); + return; + } + + logger.info(`${signal} received, shutting down gracefully`); + + // 비동기 cleanup 실행 후 프로세스 종료 + this.cleanup() + .then(() => process.exit(0)) + .catch((error) => { + logger.error('Error during graceful shutdown:', error); + process.exit(1); + }); + } +} + +/** + * Express 서버의 시작과 lifecycle을 관리하는 메인 클래스 + */ +class ServerManager { + private server?: Server; + private shutdownManager?: GracefulShutdownManager; + private readonly port = parseInt(process.env.PORT || '8080', 10); + + /** + * 서버를 초기화하고 시작 + */ + async start(): Promise { + try { + await this.initializeServices(); + this.server = this.createServer(); + this.setupShutdownHandlers(); // 시그널 핸들러 등록 + + logger.info('Server started successfully'); + } catch (error) { + logger.error('Failed to start server:', error); process.exit(1); } - }; - - // 시그널 핸들러 등록 - process.on('SIGTERM', async () => gracefulShutdown('SIGTERM')); - process.on('SIGINT', async () => gracefulShutdown('SIGINT')); - - // 예상치 못한 에러 처리 - process.on('uncaughtException', async (error) => { - logger.error('Uncaught Exception:', error); - await gracefulShutdown('UNCAUGHT_EXCEPTION'); - }); - - process.on('unhandledRejection', async (reason, promise) => { - logger.error('Unhandled Rejection at:', promise, 'reason:', reason); - await gracefulShutdown('UNHANDLED_REJECTION'); - }); - - // 강제 종료 타이머 (10초) - const forceExitTimer = setTimeout(() => { - logger.error('Could not close connections in time, forcefully shutting down'); - process.exit(1); - }, 10000); - - // graceful shutdown이 완료되면 타이머 해제 - process.on('exit', () => { - clearTimeout(forceExitTimer); - }); + } + + /** + * 데이터베이스 등 필요한 서비스들을 초기화 + */ + private async initializeServices(): Promise { + // Sentry 초기화 (에러 모니터링을 위해 가장 먼저) + initSentry(); + logger.info('Sentry initialized successfully'); + + // Cache 초기화 + initCache(); + logger.info('Cache initialized successfully'); + + // 데이터베이스 초기화 + await initializeDatabase(); + logger.info('Database initialized successfully'); + } + + /** + * Express 서버 인스턴스를 생성하고 시작 + */ + private createServer(): Server { + const server = app.listen(this.port, () => { + logger.info(`Server running on port ${this.port}`); + logger.info(`Environment: ${process.env.NODE_ENV}`); + + // 개발 환경에서만 API 문서 URL 표시 + if (process.env.NODE_ENV !== 'production') { + logger.info(`API Docs: http://localhost:${this.port}/api-docs`); + } + + logger.info(`Health Check: http://localhost:${this.port}/health`); + }); + + return server; + } + + /** + * 프로세스 시그널 핸들러와 에러 핸들러를 설정 + */ + private setupShutdownHandlers(): void { + if (!this.server) { + throw new Error('Server not initialized'); + } + + // shutdown manager 인스턴스 생성 + this.shutdownManager = new GracefulShutdownManager(this.server); + + // Graceful shutdown 시그널 핸들러 등록 + process.on('SIGTERM', () => { + if (this.shutdownManager) { + this.shutdownManager.handleShutdown('SIGTERM'); + } + }); + + process.on('SIGINT', () => { + if (this.shutdownManager) { + this.shutdownManager.handleShutdown('SIGINT'); + } + }); + + // 치명적 에러 발생 시 즉시 종료 + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error); + process.exit(1); // graceful shutdown 없이 즉시 종료 + }); + + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); + + // 개발 환경에서는 더 엄격하게 처리 + if (process.env.NODE_ENV === 'development') { + process.exit(1); + } + }); + } } -// 서버 시작 -startServer(); +// 애플리케이션 진입점 +const serverManager = new ServerManager(); +serverManager.start().catch((error) => { + logger.error('Fatal error during server startup:', error); + process.exit(1); +}); \ No newline at end of file From 7959cb30a070e103c81b89a5c8fa6964abb069a4 Mon Sep 17 00:00:00 2001 From: Nuung Date: Sat, 2 Aug 2025 11:31:48 +0900 Subject: [PATCH 7/7] =?UTF-8?q?modify:=20index=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=ED=99=94,=20=EA=B3=A0=EB=8F=84=ED=99=94,=20?= =?UTF-8?q?=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81,=20=EB=A6=B0=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 13 +++++++------ src/index.ts | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app.ts b/src/app.ts index eb0c4cb..d7d8548 100644 --- a/src/app.ts +++ b/src/app.ts @@ -30,9 +30,10 @@ app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use( cors({ - origin: process.env.NODE_ENV === 'production' - ? process.env.ALLOWED_ORIGINS?.split(',').map(origin => origin.trim()) - : 'http://localhost:3000', + origin: + process.env.NODE_ENV === 'production' + ? process.env.ALLOWED_ORIGINS?.split(',').map((origin) => origin.trim()) + : 'http://localhost:3000', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'access_token', 'refresh_token'], credentials: true, @@ -49,8 +50,8 @@ app.get('/health', async (req: Request, res: Response) => { environment: process.env.NODE_ENV, services: { sentry: false, - cache: false - } + cache: false, + }, }; // Sentry 상태 확인 @@ -86,4 +87,4 @@ app.use((req: Request, res: Response, next: NextFunction) => { app.use(errorHandlingMiddleware); -export default app; \ No newline at end of file +export default app; diff --git a/src/index.ts b/src/index.ts index 3705e07..f876207 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ class GracefulShutdownManager implements ShutdownHandler { private isShuttingDown = false; private readonly shutdownTimeout = 10000; // 10초 강제 종료 타이머 - constructor(private server: Server) { } + constructor(private server: Server) {} /** * 모든 연결을 안전하게 정리하고 서버를 종료 @@ -186,4 +186,4 @@ const serverManager = new ServerManager(); serverManager.start().catch((error) => { logger.error('Fatal error during server startup:', error); process.exit(1); -}); \ No newline at end of file +});