10 KiB
10 KiB
📋 Phase 3.11: DDLAuditLogger Raw Query 전환 계획
📋 개요
DDLAuditLogger는 8개의 Prisma 호출이 있으며, DDL 실행 감사 로그 관리를 담당하는 서비스입니다.
📊 기본 정보
| 항목 | 내용 |
|---|---|
| 파일 위치 | backend-node/src/services/ddlAuditLogger.ts |
| 파일 크기 | 350 라인 |
| Prisma 호출 | 0개 (전환 완료) |
| 현재 진행률 | 8/8 (100%) ✅ 전환 완료 |
| 복잡도 | 중간 (통계 쿼리, $executeRaw) |
| 우선순위 | 🟡 중간 (Phase 3.11) |
| 상태 | ✅ 완료 |
🎯 전환 목표
- ⏳ 8개 모든 Prisma 호출을
db.ts의query(),queryOne()함수로 교체 - ⏳ DDL 감사 로그 기능 정상 동작
- ⏳ 통계 쿼리 전환 (GROUP BY, COUNT, ORDER BY)
- ⏳ $executeRaw → query 전환
- ⏳ $queryRawUnsafe → query 전환
- ⏳ 동적 WHERE 조건 생성
- ⏳ TypeScript 컴파일 성공
- ⏳ Prisma import 완전 제거
🔍 Prisma 사용 현황 분석
주요 Prisma 호출 (8개)
1. logDDLStart() - DDL 시작 로그 (INSERT)
// Line 27
const logEntry = await prisma.$executeRaw`
INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES (
${executionId}, ${ddlType}, ${tableName}, 'in_progress',
${executedBy}, ${companyCode}, NOW(), ${JSON.stringify(metadata)}::jsonb
)
`;
2. getAuditLogs() - 감사 로그 목록 조회 (SELECT with filters)
// Line 162
const logs = await prisma.$queryRawUnsafe(query, ...params);
- 동적 WHERE 조건 생성
- 페이징 (OFFSET, LIMIT)
- 정렬 (ORDER BY)
3. getAuditStats() - 통계 조회 (복합 쿼리)
// Line 199 - 총 통계
const totalStats = (await prisma.$queryRawUnsafe(
`SELECT
COUNT(*) as total_executions,
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful,
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
AVG(EXTRACT(EPOCH FROM (completed_at - started_at))) as avg_duration
FROM ddl_audit_logs
WHERE ${whereClause}`
)) as any[];
// Line 212 - DDL 타입별 통계
const ddlTypeStats = (await prisma.$queryRawUnsafe(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`
)) as any[];
// Line 224 - 사용자별 통계
const userStats = (await prisma.$queryRawUnsafe(
`SELECT executed_by, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY executed_by
ORDER BY count DESC
LIMIT 10`
)) as any[];
// Line 237 - 최근 실패 로그
const recentFailures = (await prisma.$queryRawUnsafe(
`SELECT * FROM ddl_audit_logs
WHERE status = 'failed' AND ${whereClause}
ORDER BY started_at DESC
LIMIT 5`
)) as any[];
4. getExecutionHistory() - 실행 이력 조회
// Line 287
const history = await prisma.$queryRawUnsafe(
`SELECT * FROM ddl_audit_logs
WHERE table_name = $1 AND company_code = $2
ORDER BY started_at DESC
LIMIT $3`,
tableName,
companyCode,
limit
);
5. cleanupOldLogs() - 오래된 로그 삭제
// Line 320
const result = await prisma.$executeRaw`
DELETE FROM ddl_audit_logs
WHERE started_at < NOW() - INTERVAL '${retentionDays} days'
AND company_code = ${companyCode}
`;
💡 전환 전략
1단계: $executeRaw 전환 (2개)
logDDLStart()- INSERTcleanupOldLogs()- DELETE
2단계: 단순 $queryRawUnsafe 전환 (1개)
getExecutionHistory()- 파라미터 바인딩 있음
3단계: 복잡한 $queryRawUnsafe 전환 (1개)
getAuditLogs()- 동적 WHERE 조건
4단계: 통계 쿼리 전환 (4개)
getAuditStats()내부의 4개 쿼리- GROUP BY, CASE WHEN, AVG, EXTRACT
💻 전환 예시
예시 1: $executeRaw → query (INSERT)
변경 전:
const logEntry = await prisma.$executeRaw`
INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES (
${executionId}, ${ddlType}, ${tableName}, 'in_progress',
${executedBy}, ${companyCode}, NOW(), ${JSON.stringify(metadata)}::jsonb
)
`;
변경 후:
await query(
`INSERT INTO ddl_audit_logs (
execution_id, ddl_type, table_name, status,
executed_by, company_code, started_at, metadata
) VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7::jsonb)`,
[
executionId,
ddlType,
tableName,
"in_progress",
executedBy,
companyCode,
JSON.stringify(metadata),
]
);
예시 2: 동적 WHERE 조건
변경 전:
let query = `SELECT * FROM ddl_audit_logs WHERE 1=1`;
const params: any[] = [];
if (filters.ddlType) {
query += ` AND ddl_type = ?`;
params.push(filters.ddlType);
}
const logs = await prisma.$queryRawUnsafe(query, ...params);
변경 후:
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (filters.ddlType) {
conditions.push(`ddl_type = $${paramIndex++}`);
params.push(filters.ddlType);
}
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `SELECT * FROM ddl_audit_logs ${whereClause}`;
const logs = await query<any>(sql, params);
예시 3: 통계 쿼리 (GROUP BY)
변경 전:
const ddlTypeStats = (await prisma.$queryRawUnsafe(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`
)) as any[];
변경 후:
const ddlTypeStats = await query<{ ddl_type: string; count: string }>(
`SELECT ddl_type, COUNT(*) as count
FROM ddl_audit_logs
WHERE ${whereClause}
GROUP BY ddl_type
ORDER BY count DESC`,
params
);
🔧 기술적 고려사항
1. JSON 필드 처리
metadata 필드는 JSONB 타입으로, INSERT 시 ::jsonb 캐스팅 필요:
JSON.stringify(metadata) + "::jsonb";
2. 날짜/시간 함수
NOW()- 현재 시간INTERVAL '30 days'- 날짜 간격EXTRACT(EPOCH FROM ...)- 초 단위 변환
3. CASE WHEN 집계
COUNT(CASE WHEN status = 'success' THEN 1 END) as successful
4. 동적 WHERE 조건
여러 필터를 조합하여 WHERE 절 생성:
- ddlType
- tableName
- status
- executedBy
- dateRange (startDate, endDate)
✅ 전환 완료 내역
전환된 Prisma 호출 (8개)
logDDLExecution()- DDL 실행 로그 INSERT- Before:
prisma.$executeRaw - After:
query()with 7 parameters
- Before:
getAuditLogs()- 감사 로그 목록 조회- Before:
prisma.$queryRawUnsafe - After:
query<any>()with dynamic WHERE clause
- Before:
getDDLStatistics()- 통계 조회 (4개 쿼리)- Before: 4x
prisma.$queryRawUnsafe - After: 4x
query<any>()- totalStats: 전체 실행 통계 (CASE WHEN 집계)
- ddlTypeStats: DDL 타입별 통계 (GROUP BY)
- userStats: 사용자별 통계 (GROUP BY, LIMIT 10)
- recentFailures: 최근 실패 로그 (WHERE success = false)
- Before: 4x
getTableDDLHistory()- 테이블별 DDL 히스토리- Before:
prisma.$queryRawUnsafe - After:
query<any>()with table_name filter
- Before:
cleanupOldLogs()- 오래된 로그 삭제- Before:
prisma.$executeRaw - After:
query()with date filter
- Before:
주요 기술적 개선사항
- 파라미터 바인딩: PostgreSQL
$1, $2, ...스타일로 통일 - 동적 WHERE 조건: 파라미터 인덱스 자동 증가 로직 유지
- 통계 쿼리: CASE WHEN, GROUP BY, SUM 등 복잡한 집계 쿼리 완벽 전환
- 에러 처리: 기존 try-catch 구조 유지
- 로깅: logger 유틸리티 활용 유지
코드 정리
import { PrismaClient }제거const prisma = new PrismaClient()제거import { query, queryOne }추가- 모든 타입 정의 유지
- TypeScript 컴파일 성공
- Linter 오류 없음
📝 원본 전환 체크리스트
1단계: Prisma 호출 전환 (✅ 완료)
logDDLStart()- INSERT ($executeRaw → query)logDDLComplete()- UPDATE (이미 query 사용 중일 가능성)logDDLError()- UPDATE (이미 query 사용 중일 가능성)getAuditLogs()- SELECT with filters ($queryRawUnsafe → query)getAuditStats()내 4개 쿼리:- totalStats (집계 쿼리)
- ddlTypeStats (GROUP BY)
- userStats (GROUP BY + LIMIT)
- recentFailures (필터 + ORDER BY + LIMIT)
getExecutionHistory()- SELECT with params ($queryRawUnsafe → query)cleanupOldLogs()- DELETE ($executeRaw → query)
2단계: 코드 정리
- import 문 수정 (
prisma→query, queryOne) - Prisma import 완전 제거
- 타입 정의 확인
3단계: 테스트
- 단위 테스트 작성 (8개)
- DDL 시작 로그 테스트
- DDL 완료 로그 테스트
- 감사 로그 목록 조회 테스트
- 통계 조회 테스트
- 실행 이력 조회 테스트
- 오래된 로그 삭제 테스트
- 통합 테스트 작성 (3개)
- 전체 DDL 실행 플로우 테스트
- 필터링 및 페이징 테스트
- 통계 정확성 테스트
- 성능 테스트
- 대량 로그 조회 성능
- 통계 쿼리 성능
4단계: 문서화
- 전환 완료 문서 업데이트
- 주요 변경사항 기록
- 성능 벤치마크 결과
🎯 예상 난이도 및 소요 시간
- 난이도: ⭐⭐⭐ (중간)
- 복잡한 통계 쿼리 (GROUP BY, CASE WHEN)
- 동적 WHERE 조건 생성
- JSON 필드 처리
- 예상 소요 시간: 1~1.5시간
- Prisma 호출 전환: 30분
- 테스트: 20분
- 문서화: 10분
📌 참고사항
관련 서비스
DDLExecutionService- DDL 실행 (이미 전환 완료)DDLSafetyValidator- DDL 안전성 검증
의존성
../database/db- query, queryOne 함수../types/ddl- DDL 관련 타입../utils/logger- 로깅
상태: ⏳ 대기 중
특이사항: 통계 쿼리, JSON 필드, 동적 WHERE 조건 포함