응답 속도 최적화
캐시 우선 전략
캐시 없이 vs 캐시 있을 때
// ❌캐시 없이 (매번 AI API 호출) async function analyzeWithoutCache(url: string): Promise<AnalysisResult> { console.time('AI 분석'); const result = await geminiService.analyze(url); // 20초 console.timeEnd('AI 분석'); return result; } // 결과: 평균 20초 // 캐시 적용 async function analyzeWithCache(url: string): Promise<AnalysisResult> { console.time('전체 응답'); // 1단계: Redis 캐시 console.time('Redis 확인'); let result = await redisCache.get(url); console.timeEnd('Redis 확인'); if (result) { console.log('Redis 캐시 히트'); console.timeEnd('전체 응답'); return result; } // 2단계: DB 캐시 console.time('DB 확인'); result = await dbCache.get(url); console.timeEnd('DB 확인'); if (result) { console.log('DB 캐시 히트'); // Redis에도 저장 (다음 요청 최적화) await redisCache.set(url, result, 3600); console.timeEnd('전체 응답'); return result; } // 3단계: 메모리 캐시 result = memoryCache.get(url); if (result) { console.log('메모리 캐시 히트'); console.timeEnd('전체 응답'); return result; } // 4단계: AI API 호출 console.time('AI 호출'); result = await geminiService.analyze(url); console.timeEnd('AI 호출'); // 모든 캐시 레이어에 저장 await Promise.all([ redisCache.set(url, result, 3600), dbCache.set(url, result), memoryCache.set(url, result) ]); console.timeEnd('전체 응답'); return result; }
캐시 히트시 결과 반환 시간 약 0.5초
Gzip 압축으로 응답 크기 감소
압축 전후 비교
// 압축 미들웨어 설정 import compression from 'compression'; app.use(compression());
Connection Pooling - DB 연결 재사용
Prisma Connection Pool 최적화
// prisma/schema.prisma datasource db { provider = "sqlite" url = env("DATABASE_URL") } // 연결 풀 설정 const prisma = new PrismaClient({ log: ['warn', 'error'], datasources: { db: { url: process.env.DATABASE_URL, }, }, // 연결 풀 옵션 __internal: { engine: { pool_timeout: 10, // 연결 대기 시간 (초) pool_size: 3, // 최대 연결 수 (SQLite는 작게) connection_limit: 10, // 전체 연결 제한 }, }, });
연결 재사용 패턴
class DatabaseService { private static instance: DatabaseService; private prisma: PrismaClient; private constructor() { this.prisma = new PrismaClient({ // 연결 풀 설정 log: process.env.NODE_ENV === 'development' ? ['error'] : [], }); // 연결 상태 모니터링 this.monitorConnections(); } private async monitorConnections() { setInterval(async () => { try { await this.prisma.$queryRaw`SELECT 1`; console.log('DB 연결 정상'); } catch (error) { console.error('DB 연결 실패:', error.message); } }, 30000); // 30초마다 체크 } // 연결 정리 (서버 종료 시) async disconnect() { await this.prisma.$disconnect(); console.log('DB 연결 정리 완료'); } }
메모리 최적화
1. Node.js 힙 메모리 제한 - 384MB로 고정
메모리 제한 설정:
# Dockerfile에서 메모리 제한 설정 FROM node:18-alpine # Node.js 힙 메모리 제한 CMD ["node", "--max-old-space-size=384", "dist/app.js"] # ↑ 384MB로 제한 (전체 RAM 1GB 중 38%)
2. Redis 메모리 제한 - 64MB + LRU 정책
Redis 메모리 설정:
# redis.conf 설정 maxmemory 64mb # 최대 메모리 64MB maxmemory-policy allkeys-lru # LRU 정책으로 오래된 키 삭제 maxmemory-samples 5 # LRU 샘플링 키 개수 # 또는 Docker에서 설정 docker run -d \ --name redis-cache \ -p 6379:6379 \ redis:alpine \ redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru
LRU (Least Recently Used)
Redis 메모리가 가득 찰 때 1. 가장 오래전에 사용된 키 찾음 2. 해당 키 삭제 3. 새로운 데이터 저장 4. 이 과정을 자동으로 반복 예시 캐시 상태: [키A(1시간전), 키B(30분전), 키C(10분전)] 새 데이터 저장 시: 키A 삭제 → 새 데이터 저장
3. Alpine Linux - 최소 OS 이미지
이미지 크기 비교:
# 일반 Ubuntu 기반 (크기: ~200MB) FROM node:18 RUN apt-get update && apt-get install -y curl git COPY . . RUN npm install # 최종 이미지 크기: ~500MB # Alpine 기반 (크기: ~15MB) FROM node:18-alpine RUN apk add --no-cache curl git COPY . . RUN npm ci --only=production # 최종 이미지 크기: ~150MB (70% 감소)
Alpine 최적화 팁
FROM node:18-alpine # 한 번에 필요한 패키지 설치 (레이어 최소화) RUN apk add --no-cache \ curl \ git \ python3 \ make \ g++ \ && rm -rf /var/cache/apk/* # 보안을 위한 비 root 사용자 RUN addgroup -g 1001 -S nodejs && \ adduser -S backend -u 1001 # 불필요한 파일 제거 RUN rm -rf /opt/yarn* /usr/local/bin/yarn* \ /usr/local/bin/yarnpkg* \ /root/.npm /tmp/* USER backend
4. Multi-stage Build - 런타임 이미지 최소화
빌드 단계와 런타임 단계 분리
# 빌드 스테이지 (큰 이미지 OK) FROM node:18-alpine AS builder WORKDIR /app # 개발 도구들 설치 (빌드에만 필요) RUN apk add --no-cache python3 make g++ git # 소스코드 복사 및 빌드 COPY package*.json ./ COPY shared/package*.json ./shared/ RUN npm ci COPY . . RUN npm run build RUN cd shared && npm run build # node_modules에서 프로덕션 의존성만 추출 RUN npm ci --only=production && npm cache clean --force # 런타임 스테이지 (최소 이미지) FROM node:18-alpine WORKDIR /app # 런타임에 필요한 최소 도구만 설치 RUN apk add --no-cache curl tini # 비 root 사용자 생성 RUN addgroup -g 1001 -S nodejs && \ adduser -S backend -u 1001 # 빌드된 결과물만 복사 (소스코드, 개발 도구 제외) COPY --from=builder --chown=backend:nodejs /app/dist ./dist COPY --from=builder --chown=backend:nodejs /app/node_modules ./node_modules COPY --from=builder --chown=backend:nodejs /app/shared/dist ./shared/dist COPY --from=builder --chown=backend:nodejs /app/package*.json ./ USER backend EXPOSE 3001 # tini로 PID 1 프로세스 관리 ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "--max-old-space-size=384", "dist/app.js"]

Performance Best Practices Using Express in Production
Discover performance and reliability best practices for Express apps in production, covering code optimizations and environment setups for optimal performance.
Best practices
Hints, tips and guidelines for writing clean, reliable Dockerfiles