Criti.AI 백엔드 성능 최적화

Category
Criti AI
Status
Published
Tags
성능최적화
Description
Published
Slug

응답 속도 최적화

캐시 우선 전략

캐시 없이 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
Docker DocumentationDocker DocumentationBest practices