Skip to content

Commit 0a83c21

Browse files
committed
modify: index 파일 모듈화, 고도화, 리펙토링
1 parent 919660f commit 0a83c21

File tree

2 files changed

+161
-64
lines changed

2 files changed

+161
-64
lines changed

src/app.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,12 @@ import router from '@/routes';
1111
import { NotFoundError } from '@/exception';
1212

1313
import { options } from '@/configs/swagger.config';
14-
import { initSentry, getSentryStatus } from '@/configs/sentry.config';
15-
import { initCache, getCacheStatus } from '@/configs/cache.config';
14+
import { getSentryStatus } from '@/configs/sentry.config';
15+
import { getCacheStatus } from '@/configs/cache.config';
1616
import { errorHandlingMiddleware } from '@/middlewares/errorHandling.middleware';
1717

1818
dotenv.config();
1919

20-
initSentry(); // Sentry 초기화
21-
initCache(); // Redis 캐시 초기화
22-
2320
const app: Application = express();
2421

2522
// 실제 클라이언트 IP를 알기 위한 trust proxy 설정

src/index.ts

Lines changed: 159 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,189 @@
11
import app from '@/app';
22
import logger from '@/configs/logger.config';
3-
import { closeCache } from './configs/cache.config';
3+
import { closeCache, initCache } from './configs/cache.config';
44
import { closeDatabase, initializeDatabase } from './configs/db.config';
55
import { Server } from 'http';
6+
import { initSentry } from './configs/sentry.config';
67

7-
const port = parseInt(process.env.PORT || '8080', 10);
8+
interface ShutdownHandler {
9+
cleanup(): Promise<void>;
10+
}
811

9-
async function startServer() {
10-
try {
11-
// 데이터베이스 초기화
12-
await initializeDatabase();
13-
logger.info('데이터베이스 초기화 완료');
12+
/**
13+
* 서버의 graceful shutdown을 담당하는 매니저 클래스
14+
*/
15+
class GracefulShutdownManager implements ShutdownHandler {
16+
private isShuttingDown = false;
17+
private readonly shutdownTimeout = 10000; // 10초 강제 종료 타이머
1418

15-
// 서버 시작
16-
const server = app.listen(port, () => {
17-
logger.info(`Server running on port ${port}`);
18-
logger.info(`Environment: ${process.env.NODE_ENV}`);
19-
if (process.env.NODE_ENV !== 'production') {
20-
logger.info(`API Docs: http://localhost:${port}/api-docs`);
21-
}
22-
logger.info(`Health Check: http://localhost:${port}/health`);
23-
});
19+
constructor(private server: Server) { }
2420

25-
// Graceful shutdown 핸들러 설정
26-
setupGracefulShutdown(server);
27-
} catch (error) {
28-
logger.error('서버 시작 중 오류 발생:', error);
29-
process.exit(1);
30-
}
31-
}
21+
/**
22+
* 모든 연결을 안전하게 정리하고 서버를 종료
23+
*/
24+
async cleanup(): Promise<void> {
25+
if (this.isShuttingDown) {
26+
return;
27+
}
3228

33-
function setupGracefulShutdown(server: Server) {
34-
// 기본적인 graceful shutdown 추가
35-
const gracefulShutdown = async (signal: string) => {
36-
logger.info(`${signal} received, shutting down gracefully`);
29+
this.isShuttingDown = true;
30+
31+
// 강제 종료 타이머 설정 (데드락 방지)
32+
const forceExitTimer = setTimeout(() => {
33+
logger.error('Could not close connections in time, forcefully shutting down');
34+
process.exit(1);
35+
}, this.shutdownTimeout);
3736

3837
try {
3938
// HTTP 서버 종료
4039
await new Promise<void>((resolve) => {
41-
server.close(() => {
40+
this.server.close(() => {
4241
logger.info('HTTP server closed');
4342
resolve();
4443
});
4544
});
4645

4746
// 데이터베이스 연결 종료
4847
await closeDatabase();
48+
logger.info('Database connections closed');
4949

5050
// 캐시 연결 종료
5151
await closeCache();
52+
logger.info('Cache connections closed');
5253

54+
clearTimeout(forceExitTimer); // 정상 종료 시 강제 타이머 해제
5355
logger.info('Graceful shutdown completed');
54-
process.exit(0);
5556
} catch (error) {
56-
logger.error('Graceful shutdown 중 오류 발생:', error);
57+
clearTimeout(forceExitTimer);
58+
throw error;
59+
}
60+
}
61+
62+
/**
63+
* 시그널을 받아 graceful shutdown을 시작
64+
*/
65+
handleShutdown(signal: string): void {
66+
if (this.isShuttingDown) {
67+
logger.info(`Already shutting down, ignoring ${signal}`);
68+
return;
69+
}
70+
71+
logger.info(`${signal} received, shutting down gracefully`);
72+
73+
// 비동기 cleanup 실행 후 프로세스 종료
74+
this.cleanup()
75+
.then(() => process.exit(0))
76+
.catch((error) => {
77+
logger.error('Error during graceful shutdown:', error);
78+
process.exit(1);
79+
});
80+
}
81+
}
82+
83+
/**
84+
* Express 서버의 시작과 lifecycle을 관리하는 메인 클래스
85+
*/
86+
class ServerManager {
87+
private server?: Server;
88+
private shutdownManager?: GracefulShutdownManager;
89+
private readonly port = parseInt(process.env.PORT || '8080', 10);
90+
91+
/**
92+
* 서버를 초기화하고 시작
93+
*/
94+
async start(): Promise<void> {
95+
try {
96+
await this.initializeServices();
97+
this.server = this.createServer();
98+
this.setupShutdownHandlers(); // 시그널 핸들러 등록
99+
100+
logger.info('Server started successfully');
101+
} catch (error) {
102+
logger.error('Failed to start server:', error);
57103
process.exit(1);
58104
}
59-
};
60-
61-
// 시그널 핸들러 등록
62-
process.on('SIGTERM', async () => gracefulShutdown('SIGTERM'));
63-
process.on('SIGINT', async () => gracefulShutdown('SIGINT'));
64-
65-
// 예상치 못한 에러 처리
66-
process.on('uncaughtException', async (error) => {
67-
logger.error('Uncaught Exception:', error);
68-
await gracefulShutdown('UNCAUGHT_EXCEPTION');
69-
});
70-
71-
process.on('unhandledRejection', async (reason, promise) => {
72-
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
73-
await gracefulShutdown('UNHANDLED_REJECTION');
74-
});
75-
76-
// 강제 종료 타이머 (10초)
77-
const forceExitTimer = setTimeout(() => {
78-
logger.error('Could not close connections in time, forcefully shutting down');
79-
process.exit(1);
80-
}, 10000);
81-
82-
// graceful shutdown이 완료되면 타이머 해제
83-
process.on('exit', () => {
84-
clearTimeout(forceExitTimer);
85-
});
105+
}
106+
107+
/**
108+
* 데이터베이스 등 필요한 서비스들을 초기화
109+
*/
110+
private async initializeServices(): Promise<void> {
111+
// Sentry 초기화 (에러 모니터링을 위해 가장 먼저)
112+
initSentry();
113+
logger.info('Sentry initialized successfully');
114+
115+
// Cache 초기화
116+
initCache();
117+
logger.info('Cache initialized successfully');
118+
119+
// 데이터베이스 초기화
120+
await initializeDatabase();
121+
logger.info('Database initialized successfully');
122+
}
123+
124+
/**
125+
* Express 서버 인스턴스를 생성하고 시작
126+
*/
127+
private createServer(): Server {
128+
const server = app.listen(this.port, () => {
129+
logger.info(`Server running on port ${this.port}`);
130+
logger.info(`Environment: ${process.env.NODE_ENV}`);
131+
132+
// 개발 환경에서만 API 문서 URL 표시
133+
if (process.env.NODE_ENV !== 'production') {
134+
logger.info(`API Docs: http://localhost:${this.port}/api-docs`);
135+
}
136+
137+
logger.info(`Health Check: http://localhost:${this.port}/health`);
138+
});
139+
140+
return server;
141+
}
142+
143+
/**
144+
* 프로세스 시그널 핸들러와 에러 핸들러를 설정
145+
*/
146+
private setupShutdownHandlers(): void {
147+
if (!this.server) {
148+
throw new Error('Server not initialized');
149+
}
150+
151+
// shutdown manager 인스턴스 생성
152+
this.shutdownManager = new GracefulShutdownManager(this.server);
153+
154+
// Graceful shutdown 시그널 핸들러 등록
155+
process.on('SIGTERM', () => {
156+
if (this.shutdownManager) {
157+
this.shutdownManager.handleShutdown('SIGTERM');
158+
}
159+
});
160+
161+
process.on('SIGINT', () => {
162+
if (this.shutdownManager) {
163+
this.shutdownManager.handleShutdown('SIGINT');
164+
}
165+
});
166+
167+
// 치명적 에러 발생 시 즉시 종료
168+
process.on('uncaughtException', (error) => {
169+
logger.error('Uncaught Exception:', error);
170+
process.exit(1); // graceful shutdown 없이 즉시 종료
171+
});
172+
173+
process.on('unhandledRejection', (reason, promise) => {
174+
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
175+
176+
// 개발 환경에서는 더 엄격하게 처리
177+
if (process.env.NODE_ENV === 'development') {
178+
process.exit(1);
179+
}
180+
});
181+
}
86182
}
87183

88-
// 서버 시작
89-
startServer();
184+
// 애플리케이션 진입점
185+
const serverManager = new ServerManager();
186+
serverManager.start().catch((error) => {
187+
logger.error('Fatal error during server startup:', error);
188+
process.exit(1);
189+
});

0 commit comments

Comments
 (0)