Skip to content

Commit f29e3cd

Browse files
committed
feature: 추후 Ops 를 위한 헬스 체크 세팅, app.ts 리뉴얼
1 parent ea8337f commit f29e3cd

File tree

5 files changed

+166
-38
lines changed

5 files changed

+166
-38
lines changed

src/app.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,94 @@
11
import 'reflect-metadata';
2-
import express, { Application } from 'express';
2+
import express, { Application, Request, Response, NextFunction } from 'express';
33
import dotenv from 'dotenv';
44
import cors from 'cors';
55
import cookieParser from 'cookie-parser';
6-
import router from './routes';
76
import swaggerUi from 'swagger-ui-express';
87
import swaggerJSDoc from 'swagger-jsdoc';
8+
9+
import logger from '@/configs/logger.config';
10+
import router from '@/routes';
11+
import { NotFoundError } from '@/exception';
12+
913
import { options } from '@/configs/swagger.config';
10-
import { errorHandlingMiddleware } from './middlewares/errorHandling.middleware';
11-
import { NotFoundError } from './exception';
1214
import { initSentry } from '@/configs/sentry.config';
15+
import { initCache } from '@/configs/cache.config';
16+
import { errorHandlingMiddleware } from '@/middlewares/errorHandling.middleware';
1317

1418
dotenv.config();
1519

16-
// Sentry 초기화
17-
initSentry();
20+
initSentry(); // Sentry 초기화
21+
initCache(); // Redis 캐시 초기화
1822

1923
const app: Application = express();
24+
2025
// 실제 클라이언트 IP를 알기 위한 trust proxy 설정
21-
app.set('trust proxy', true);
26+
app.set('trust proxy', process.env.NODE_ENV === 'production');
27+
2228
const swaggerSpec = swaggerJSDoc(options);
2329

2430
app.use(cookieParser());
25-
app.use(express.json());
26-
app.use(express.urlencoded({ extended: true }));
31+
app.use(express.json({ limit: '10mb' })); // 파일 업로드 대비
32+
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
33+
2734
app.use(
2835
cors({
29-
origin: process.env.NODE_ENV === 'production' ? process.env.ALLOWED_ORIGINS?.split(',') : 'http://localhost:3000',
36+
origin: process.env.NODE_ENV === 'production'
37+
? process.env.ALLOWED_ORIGINS?.split(',').map(origin => origin.trim())
38+
: 'http://localhost:3000',
3039
methods: ['GET', 'POST'],
3140
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'access_token', 'refresh_token'],
3241
credentials: true,
3342
}),
3443
);
3544

36-
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
45+
// 헬스체크 엔드포인트
46+
app.get('/health', async (req: Request, res: Response) => {
47+
// 기본 정보
48+
const healthData = {
49+
status: 'OK',
50+
timestamp: new Date().toISOString(),
51+
uptime: process.uptime(),
52+
environment: process.env.NODE_ENV,
53+
services: {
54+
sentry: false,
55+
cache: false
56+
}
57+
};
58+
59+
// Sentry 상태 확인
60+
try {
61+
const { getSentryStatus } = await import('./configs/sentry.config.ts');
62+
healthData.services.sentry = getSentryStatus();
63+
} catch (error) {
64+
healthData.services.sentry = false;
65+
logger.error('Failed to health check for sentry:', error);
66+
}
67+
68+
// Cache 상태 확인
69+
try {
70+
const { getCacheStatus } = await import('./configs/cache.config.ts');
71+
healthData.services.cache = await getCacheStatus();
72+
} catch (error) {
73+
healthData.services.cache = false;
74+
logger.error('Failed to health check for cache:', error);
75+
}
76+
77+
res.status(200).json(healthData);
78+
});
79+
80+
// Swagger는 개발 환경에서만
81+
if (process.env.NODE_ENV !== 'production') {
82+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
83+
}
84+
3785
app.use('/api', router);
38-
app.use((req) => {
39-
throw new NotFoundError(`${req.url} not found`);
86+
87+
// 404 에러 핸들링 수정 (throw 대신 next 사용)
88+
app.use((req: Request, res: Response, next: NextFunction) => {
89+
next(new NotFoundError(`${req.url} not found`));
4090
});
4191

4292
app.use(errorHandlingMiddleware);
4393

44-
export default app;
94+
export default app;

src/configs/cache.config.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@ const cacheConfig: CacheConfig = {
1515

1616
// 싱글톤 캐시 인스턴스 (const로 변경하고 null 초기화)
1717
const cacheInstance: ICache = new RedisCache(cacheConfig);
18-
1918
export const cache = cacheInstance;
2019

20+
// 캐시 상태 추적 변수
21+
let cacheInitialized = false;
22+
2123
// 초기화 함수
2224
export const initCache = async (): Promise<void> => {
2325
try {
2426
await cache.connect();
27+
cacheInitialized = true;
2528
logger.info('Cache system initialized successfully');
2629
} catch (error) {
30+
cacheInitialized = false;
2731
logger.error('Failed to initialize cache system:', error);
2832
// 캐시 연결 실패해도 애플리케이션은 계속 실행
2933
logger.warn('Application will continue without cache');
@@ -34,8 +38,26 @@ export const initCache = async (): Promise<void> => {
3438
export const closeCache = async (): Promise<void> => {
3539
try {
3640
await cache.disconnect();
41+
cacheInitialized = false;
3742
logger.info('Cache system closed successfully');
3843
} catch (error) {
3944
logger.error('Failed to close cache system:', error);
4045
}
4146
};
47+
48+
// 캐시 상태 확인 함수
49+
export const getCacheStatus = async (): Promise<boolean> => {
50+
if (!cacheInitialized) {
51+
return false;
52+
}
53+
54+
try {
55+
// Redis ping 명령어로 연결 상태 확인
56+
await cache.set('health-check', 'ok', 1); // 1초 TTL로 테스트 키 설정
57+
await cache.get('health-check'); // 읽기 테스트
58+
return true;
59+
} catch (error) {
60+
logger.warn('Cache health check failed:', error);
61+
return false;
62+
}
63+
};

src/configs/db.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dotenv from 'dotenv';
22
import pg from 'pg';
3-
import logger from './logger.config';
3+
import logger from '@/configs/logger.config';
4+
45
// eslint-disable-next-line @typescript-eslint/naming-convention
56
const { Pool } = pg;
67

src/configs/sentry.config.ts

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,42 @@ import dotenv from 'dotenv';
33

44
dotenv.config();
55

6+
// Sentry 초기화 상태 추적
7+
let sentryInitialized = false;
8+
69
export const initSentry = () => {
7-
Sentry.init({
8-
dsn: process.env.SENTRY_DSN,
9-
release: process.env.NODE_ENV,
10-
11-
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
12-
tracesSampleRate: 0.1,
13-
14-
// Setting this option to true will print useful information to the console while you're setting up Sentry.
15-
debug: false,
16-
enabled: true,
17-
18-
// Capture 100% of the transactions for performance monitoring
19-
integrations: [
20-
Sentry.httpIntegration(),
21-
Sentry.expressIntegration(),
22-
],
23-
});
10+
try {
11+
if (!process.env.SENTRY_DSN) {
12+
sentryInitialized = false;
13+
return;
14+
}
15+
16+
Sentry.init({
17+
dsn: process.env.SENTRY_DSN,
18+
release: process.env.NODE_ENV,
19+
20+
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
21+
tracesSampleRate: 0.1,
22+
23+
// Setting this option to true will print useful information to the console while you're setting up Sentry.
24+
debug: false,
25+
enabled: true,
26+
27+
// Capture 100% of the transactions for performance monitoring
28+
integrations: [
29+
Sentry.httpIntegration(),
30+
Sentry.expressIntegration(),
31+
],
32+
});
33+
34+
sentryInitialized = true;
35+
} catch (error) {
36+
sentryInitialized = false;
37+
throw error;
38+
}
39+
};
40+
41+
// Sentry 상태 확인 함수
42+
export const getSentryStatus = (): boolean => {
43+
return sentryInitialized;
2444
};

src/index.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
1-
import app from './app';
2-
import logger from './configs/logger.config';
1+
import app from '@/app';
2+
import logger from '@/configs/logger.config';
33

4-
const port = process.env.PORT || 3000;
4+
const port = parseInt(process.env.PORT || '8080', 10);
55

6-
app.listen(port, () => {
7-
logger.info(`Server is running on http://localhost:${port}`);
6+
const server = app.listen(port, () => {
7+
logger.info(`Server running on port ${port}`);
8+
logger.info(`Environment: ${process.env.NODE_ENV}`);
9+
if (process.env.NODE_ENV !== 'production') {
10+
logger.info(`API Docs: http://localhost:${port}/api-docs`);
11+
}
12+
logger.info(`Health Check: http://localhost:${port}/health`);
13+
});
14+
15+
// 기본적인 graceful shutdown 추가
16+
const gracefulShutdown = (signal: string) => {
17+
logger.info(`${signal} received, shutting down gracefully`);
18+
19+
server.close(() => {
20+
logger.info('HTTP server closed');
21+
process.exit(0);
22+
});
23+
24+
// 강제 종료 타이머 (10초)
25+
setTimeout(() => {
26+
logger.error('Could not close connections in time, forcefully shutting down');
27+
process.exit(1);
28+
}, 10000);
29+
};
30+
31+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
32+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
33+
34+
// 예상치 못한 에러 처리
35+
process.on('uncaughtException', (error) => {
36+
logger.error('Uncaught Exception:', error);
37+
process.exit(1);
38+
});
39+
40+
process.on('unhandledRejection', (reason, promise) => {
41+
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
42+
process.exit(1);
843
});

0 commit comments

Comments
 (0)