ERP-node/frontend/test-scenarios/stress-test-scenarios.ts

801 lines
28 KiB
TypeScript

/**
* 🔥 스트레스 테스트 시나리오
*
* 타입 시스템의 견고함을 검증하기 위한 극한 상황 테스트
*/
import {
ComponentData,
WidgetComponent,
WebType,
ScreenDefinition,
LayoutData,
// 타입 가드 및 유틸리티
isWebType,
isButtonActionType,
isWidgetComponent,
asWidgetComponent,
ynToBoolean,
booleanToYN,
} from "@/types";
// 스트레스 테스트용 확장된 화면 정의
interface TestScreenDefinition extends ScreenDefinition {
layoutData?: LayoutData;
}
export class StressTestSuite {
private static results: Array<{
testName: string;
status: "passed" | "failed" | "warning";
duration: number;
details: string;
metrics?: Record<string, unknown>;
}> = [];
/**
* 🔥 Test 1: 대량 데이터 처리 스트레스 테스트
*/
static async testMassiveDataProcessing() {
console.log("🔥 스트레스 테스트 1: 대량 데이터 처리");
const startTime = performance.now();
try {
// 10,000개의 컴포넌트 생성 및 타입 검증
const componentCount = 10000;
const components: ComponentData[] = [];
const webTypes: WebType[] = ["text", "number", "date", "select", "checkbox", "textarea", "email", "decimal"];
console.log(`📊 ${componentCount}개의 컴포넌트 생성 중...`);
for (let i = 0; i < componentCount; i++) {
const randomWebType = webTypes[i % webTypes.length];
const component: WidgetComponent = {
id: `stress-widget-${i}`,
type: "widget",
widgetType: randomWebType,
position: { x: Math.random() * 1000, y: Math.random() * 1000 },
size: { width: 100 + Math.random() * 200, height: 30 + Math.random() * 50 },
label: `스트레스 테스트 컴포넌트 ${i}`,
columnName: `stress_column_${i}`,
required: Math.random() > 0.5,
readonly: Math.random() > 0.7,
webTypeConfig: {
maxLength: Math.floor(Math.random() * 500),
placeholder: `테스트 플레이스홀더 ${i}`,
},
};
components.push(component);
}
console.log("🔍 타입 검증 시작...");
let validComponents = 0;
let invalidComponents = 0;
// 모든 컴포넌트 타입 검증
for (const component of components) {
if (isWidgetComponent(component)) {
const widget = asWidgetComponent(component);
if (widget && isWebType(widget.widgetType)) {
validComponents++;
} else {
invalidComponents++;
}
} else {
invalidComponents++;
}
}
const endTime = performance.now();
const duration = endTime - startTime;
const metrics = {
totalComponents: componentCount,
validComponents,
invalidComponents,
processingTimeMs: duration,
componentsPerSecond: Math.round(componentCount / (duration / 1000)),
};
console.log("📈 대량 데이터 처리 결과:", metrics);
this.results.push({
testName: "대량 데이터 처리",
status: invalidComponents === 0 ? "passed" : "failed",
duration,
details: `${validComponents}/${componentCount} 컴포넌트 검증 성공`,
metrics,
});
return metrics;
} catch (error) {
const endTime = performance.now();
this.results.push({
testName: "대량 데이터 처리",
status: "failed",
duration: endTime - startTime,
details: `오류 발생: ${error}`,
});
throw error;
}
}
/**
* 🔥 Test 2: 타입 오염 및 손상 시나리오
*/
static async testTypeCorruption() {
console.log("🔥 스트레스 테스트 2: 타입 오염 및 손상");
const startTime = performance.now();
try {
// 다양한 잘못된 데이터들로 타입 시스템 공격
const corruptedInputs = [
// 잘못된 WebType들
{ webType: null, expected: false },
{ webType: undefined, expected: false },
{ webType: "", expected: false },
{ webType: "invalid_type", expected: false },
{ webType: "VARCHAR(255)", expected: false },
{ webType: "submit", expected: false }, // ButtonActionType과 혼동
{ webType: "widget", expected: false }, // ComponentType과 혼동
{ webType: 123, expected: false },
{ webType: {}, expected: false },
{ webType: [], expected: false },
{ webType: "text", expected: true }, // 유일한 올바른 값
// 잘못된 ButtonActionType들
{ buttonAction: "insert", expected: false },
{ buttonAction: "update", expected: false },
{ buttonAction: "remove", expected: false },
{ buttonAction: "text", expected: false }, // WebType과 혼동
{ buttonAction: null, expected: false },
{ buttonAction: 456, expected: false },
{ buttonAction: "save", expected: true }, // 올바른 값
];
let passedChecks = 0;
let failedChecks = 0;
console.log("🦠 타입 오염 데이터 검증 중...");
corruptedInputs.forEach((input, index) => {
if ("webType" in input) {
const isValid = isWebType(input.webType as unknown);
if (isValid === input.expected) {
passedChecks++;
} else {
failedChecks++;
console.warn(
`❌ WebType 검증 실패 #${index}:`,
input.webType,
"expected:",
input.expected,
"got:",
isValid,
);
}
}
if ("buttonAction" in input) {
const isValid = isButtonActionType(input.buttonAction as unknown);
if (isValid === input.expected) {
passedChecks++;
} else {
failedChecks++;
console.warn(
`❌ ButtonActionType 검증 실패 #${index}:`,
input.buttonAction,
"expected:",
input.expected,
"got:",
isValid,
);
}
}
});
// 극한 메모리 오염 시뮬레이션
const memoryCorruptionTest = () => {
const largeString = "x".repeat(1000000); // 1MB 문자열
const corruptedComponent = {
id: largeString,
type: "widget", // 올바른 타입이지만
widgetType: largeString, // 잘못된 웹타입 (매우 긴 문자열)
position: { x: Infinity, y: -Infinity }, // 잘못된 위치값
size: { width: NaN, height: -1 }, // 잘못된 크기값
label: null, // null 라벨
// 필수 필드들이 누락됨
};
// 더 엄격한 검증을 위해 실제 WidgetComponent 인터페이스와 비교
const isValidWidget = isWidgetComponent(corruptedComponent as unknown);
// 추가 검증: widgetType이 유효한 WebType인지 확인
const hasValidWebType = corruptedComponent.widgetType && isWebType(corruptedComponent.widgetType);
// 추가 검증: 필수 필드들이 존재하고 유효한지 확인
const hasValidStructure =
corruptedComponent.position &&
typeof corruptedComponent.position.x === "number" &&
typeof corruptedComponent.position.y === "number" &&
!isNaN(corruptedComponent.position.x) &&
!isNaN(corruptedComponent.position.y) &&
corruptedComponent.size &&
typeof corruptedComponent.size.width === "number" &&
typeof corruptedComponent.size.height === "number" &&
!isNaN(corruptedComponent.size.width) &&
!isNaN(corruptedComponent.size.height) &&
corruptedComponent.size.width > 0 &&
corruptedComponent.size.height > 0;
// 모든 검증이 통과해야 true 반환 (실제로는 모두 실패해야 함)
return isValidWidget && hasValidWebType && hasValidStructure;
};
const memoryTestResult = memoryCorruptionTest();
const endTime = performance.now();
const duration = endTime - startTime;
const metrics = {
totalChecks: corruptedInputs.length,
passedChecks,
failedChecks,
memoryCorruptionHandled: !memoryTestResult, // 오염된 컴포넌트는 거부되어야 함
duration,
};
console.log("🦠 타입 오염 테스트 결과:", metrics);
this.results.push({
testName: "타입 오염 및 손상",
status: failedChecks === 0 && metrics.memoryCorruptionHandled ? "passed" : "failed",
duration,
details: `${passedChecks}/${corruptedInputs.length} 오염 데이터 차단 성공`,
metrics,
});
return metrics;
} catch (error) {
const endTime = performance.now();
this.results.push({
testName: "타입 오염 및 손상",
status: "failed",
duration: endTime - startTime,
details: `오류 발생: ${error}`,
});
throw error;
}
}
/**
* 🔥 Test 3: 동시 작업 및 경합 상태 테스트
*/
static async testConcurrentOperations() {
console.log("🔥 스트레스 테스트 3: 동시 작업 및 경합 상태");
const startTime = performance.now();
try {
const concurrentTasks = 100;
const operationsPerTask = 100;
console.log(`${concurrentTasks}개의 동시 작업 시작 (각각 ${operationsPerTask}개 연산)...`);
// 동시에 실행될 작업들
const concurrentPromises = Array.from({ length: concurrentTasks }, async (_, taskIndex) => {
const taskResults = {
taskIndex,
successfulOperations: 0,
failedOperations: 0,
typeGuardCalls: 0,
conversionCalls: 0,
};
for (let i = 0; i < operationsPerTask; i++) {
try {
// 타입 가드 테스트
const randomWebType = ["text", "number", "invalid"][Math.floor(Math.random() * 3)];
isWebType(randomWebType as unknown);
taskResults.typeGuardCalls++;
// Y/N 변환 테스트
const randomBoolean = Math.random() > 0.5;
const ynValue = booleanToYN(randomBoolean);
const backToBoolean = ynToBoolean(ynValue);
taskResults.conversionCalls++;
if (backToBoolean === randomBoolean) {
taskResults.successfulOperations++;
} else {
taskResults.failedOperations++;
}
// 컴포넌트 생성 및 타입 가드 테스트
const component: WidgetComponent = {
id: `concurrent-${taskIndex}-${i}`,
type: "widget",
widgetType: "text",
position: { x: 0, y: 0 },
size: { width: 100, height: 30 },
label: `Concurrent ${taskIndex}-${i}`,
webTypeConfig: {},
};
if (isWidgetComponent(component)) {
taskResults.successfulOperations++;
} else {
taskResults.failedOperations++;
}
} catch {
taskResults.failedOperations++;
}
}
return taskResults;
});
// 모든 동시 작업 완료 대기
const allResults = await Promise.all(concurrentPromises);
const aggregatedResults = allResults.reduce(
(acc, result) => ({
totalTasks: acc.totalTasks + 1,
totalSuccessfulOperations: acc.totalSuccessfulOperations + result.successfulOperations,
totalFailedOperations: acc.totalFailedOperations + result.failedOperations,
totalTypeGuardCalls: acc.totalTypeGuardCalls + result.typeGuardCalls,
totalConversionCalls: acc.totalConversionCalls + result.conversionCalls,
}),
{
totalTasks: 0,
totalSuccessfulOperations: 0,
totalFailedOperations: 0,
totalTypeGuardCalls: 0,
totalConversionCalls: 0,
},
);
const endTime = performance.now();
const duration = endTime - startTime;
const metrics = {
...aggregatedResults,
concurrentTasks,
operationsPerTask,
totalOperations: concurrentTasks * operationsPerTask * 3, // 각 루프에서 3개 연산
duration,
operationsPerSecond: Math.round(
(aggregatedResults.totalSuccessfulOperations + aggregatedResults.totalFailedOperations) / (duration / 1000),
),
successRate:
(aggregatedResults.totalSuccessfulOperations /
(aggregatedResults.totalSuccessfulOperations + aggregatedResults.totalFailedOperations)) *
100,
};
console.log("⚡ 동시 작업 테스트 결과:", metrics);
this.results.push({
testName: "동시 작업 및 경합 상태",
status: metrics.successRate > 95 ? "passed" : "failed",
duration,
details: `${metrics.successRate.toFixed(2)}% 성공률`,
metrics,
});
return metrics;
} catch (error) {
const endTime = performance.now();
this.results.push({
testName: "동시 작업 및 경합 상태",
status: "failed",
duration: endTime - startTime,
details: `오류 발생: ${error}`,
});
throw error;
}
}
/**
* 🔥 Test 4: 메모리 부하 및 가비지 컬렉션 스트레스
*/
static async testMemoryStress() {
console.log("🔥 스트레스 테스트 4: 메모리 부하 및 가비지 컬렉션");
const startTime = performance.now();
try {
const iterations = 1000;
const objectsPerIteration = 1000;
console.log(`🧠 메모리 스트레스 테스트: ${iterations}회 반복, 매회 ${objectsPerIteration}개 객체 생성`);
let totalObjectsCreated = 0;
let gcTriggered = 0;
// 메모리 사용량 모니터링 (가능한 경우)
const initialMemory =
(performance as unknown as { memory?: { usedJSHeapSize: number } }).memory?.usedJSHeapSize || 0;
for (let iteration = 0; iteration < iterations; iteration++) {
// 대량의 객체 생성
const tempObjects: ComponentData[] = [];
for (let i = 0; i < objectsPerIteration; i++) {
const largeComponent: WidgetComponent = {
id: `memory-stress-${iteration}-${i}`,
type: "widget",
widgetType: "textarea",
position: { x: Math.random() * 10000, y: Math.random() * 10000 },
size: { width: Math.random() * 1000, height: Math.random() * 1000 },
label: "메모리 스트레스 테스트 컴포넌트 ".repeat(10), // 긴 문자열
columnName: `stress_test_column_with_very_long_name_${iteration}_${i}`,
placeholder: "매우 긴 플레이스홀더 텍스트 ".repeat(20),
webTypeConfig: {
maxLength: 10000,
rows: 50,
placeholder: "대용량 텍스트 영역 ".repeat(50),
validation: {
pattern: "매우 복잡한 정규식 패턴 ".repeat(10),
errorMessage: "복잡한 오류 메시지 ".repeat(10),
},
},
};
// 타입 검증
if (isWidgetComponent(largeComponent)) {
tempObjects.push(largeComponent);
totalObjectsCreated++;
}
}
// 더 적극적인 메모리 해제 (가비지 컬렉션 유도)
if (iteration % 50 === 0) {
// 더 자주 정리 (100 → 50)
tempObjects.length = 0; // 배열 초기화
// 강제적인 가비지 컬렉션 힌트 제공
if (typeof global !== "undefined" && (global as unknown as { gc?: () => void }).gc) {
(global as unknown as { gc: () => void }).gc();
gcTriggered++;
}
// 추가적인 메모리 정리 시뮬레이션
// 큰 객체들을 null로 설정하여 참조 해제
for (let cleanupIndex = 0; cleanupIndex < 10; cleanupIndex++) {
const dummyCleanup = new Array(1000).fill(null);
dummyCleanup.length = 0;
}
console.log(`🗑️ 가비지 컬렉션 시뮬레이션 (반복 ${iteration}/${iterations})`);
}
}
const finalMemory =
(performance as unknown as { memory?: { usedJSHeapSize: number } }).memory?.usedJSHeapSize || 0;
const memoryDelta = finalMemory - initialMemory;
const endTime = performance.now();
const duration = endTime - startTime;
const metrics = {
iterations,
objectsPerIteration,
totalObjectsCreated,
gcTriggered,
initialMemoryBytes: initialMemory,
finalMemoryBytes: finalMemory,
memoryDeltaBytes: memoryDelta,
memoryDeltaMB: Math.round((memoryDelta / (1024 * 1024)) * 100) / 100,
duration,
objectsPerSecond: Math.round(totalObjectsCreated / (duration / 1000)),
};
console.log("🧠 메모리 스트레스 테스트 결과:", metrics);
// 메모리 누수 체크 (매우 단순한 휴리스틱)
const suspectedMemoryLeak = metrics.memoryDeltaMB > 100; // 100MB 이상 증가 시 의심
this.results.push({
testName: "메모리 부하 및 가비지 컬렉션",
status: suspectedMemoryLeak ? "warning" : "passed",
duration,
details: `${metrics.totalObjectsCreated}개 객체 생성, 메모리 변화: ${metrics.memoryDeltaMB}MB`,
metrics,
});
return metrics;
} catch (error) {
const endTime = performance.now();
this.results.push({
testName: "메모리 부하 및 가비지 컬렉션",
status: "failed",
duration: endTime - startTime,
details: `오류 발생: ${error}`,
});
throw error;
}
}
/**
* 🔥 Test 5: API 스트레스 및 네트워크 시뮬레이션
*/
static async testAPIStress() {
console.log("🔥 스트레스 테스트 5: API 스트레스 및 네트워크 시뮬레이션");
const startTime = performance.now();
try {
// 대량의 API 요청 시뮬레이션
const apiCalls = 100;
const batchSize = 10;
console.log(`🌐 ${apiCalls}개의 API 호출을 ${batchSize}개씩 배치로 처리...`);
let successfulCalls = 0;
let failedCalls = 0;
const responseTimes: number[] = [];
// 배치별로 API 호출 시뮬레이션
for (let batch = 0; batch < Math.ceil(apiCalls / batchSize); batch++) {
const batchPromises = [];
for (let i = 0; i < batchSize && batch * batchSize + i < apiCalls; i++) {
const callIndex = batch * batchSize + i;
// API 호출 시뮬레이션 (실제로는 타입 처리 로직)
const apiCallSimulation = async () => {
const callStart = performance.now();
try {
// 복잡한 데이터 구조 생성 및 검증
const components: ComponentData[] = Array.from(
{ length: 50 },
(_, idx) =>
({
id: `stress-component-${callIndex}-${idx}`,
type: "widget" as const,
widgetType: "text" as WebType,
position: { x: idx * 10, y: idx * 5 },
size: { width: 200, height: 40 },
label: `컴포넌트 ${idx}`,
webTypeConfig: {},
}) as WidgetComponent,
);
const complexScreenData: TestScreenDefinition = {
screenId: callIndex,
screenName: `스트레스 테스트 화면 ${callIndex}`,
screenCode: `STRESS_SCREEN_${callIndex}`,
tableName: `stress_table_${callIndex}`,
tableLabel: `스트레스 테이블 ${callIndex}`,
companyCode: "COMPANY_1",
description: `API 스트레스 테스트용 화면 ${callIndex}`,
isActive: Math.random() > 0.5 ? "Y" : "N",
createdDate: new Date(),
updatedDate: new Date(),
layoutData: {
screenId: callIndex,
components,
gridSettings: {
enabled: true,
size: 10,
color: "#e0e0e0",
opacity: 0.5,
snapToGrid: true,
},
},
};
// 모든 컴포넌트 타입 검증
let validComponents = 0;
if (complexScreenData.layoutData?.components) {
for (const component of complexScreenData.layoutData.components) {
if (isWidgetComponent(component)) {
validComponents++;
}
}
}
const callEnd = performance.now();
const responseTime = callEnd - callStart;
responseTimes.push(responseTime);
const totalComponents = complexScreenData.layoutData?.components?.length || 0;
if (validComponents === totalComponents) {
successfulCalls++;
return { success: true, responseTime, validComponents };
} else {
failedCalls++;
return { success: false, responseTime, validComponents };
}
} catch (error) {
const callEnd = performance.now();
const responseTime = callEnd - callStart;
responseTimes.push(responseTime);
failedCalls++;
return { success: false, responseTime, error };
}
};
batchPromises.push(apiCallSimulation());
}
// 배치 완료 대기
await Promise.all(batchPromises);
// 배치 간 짧은 대기 (실제 네트워크 지연 시뮬레이션)
await new Promise((resolve) => setTimeout(resolve, 10));
}
const endTime = performance.now();
const duration = endTime - startTime;
// 응답 시간 통계
const avgResponseTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length;
const maxResponseTime = Math.max(...responseTimes);
const minResponseTime = Math.min(...responseTimes);
const metrics = {
totalAPICalls: apiCalls,
successfulCalls,
failedCalls,
successRate: (successfulCalls / apiCalls) * 100,
avgResponseTimeMs: Math.round(avgResponseTime * 100) / 100,
maxResponseTimeMs: Math.round(maxResponseTime * 100) / 100,
minResponseTimeMs: Math.round(minResponseTime * 100) / 100,
totalDuration: duration,
callsPerSecond: Math.round(apiCalls / (duration / 1000)),
};
console.log("🌐 API 스트레스 테스트 결과:", metrics);
this.results.push({
testName: "API 스트레스 및 네트워크 시뮬레이션",
status: metrics.successRate > 95 ? "passed" : "failed",
duration,
details: `${metrics.successRate.toFixed(2)}% 성공률, 평균 응답시간: ${metrics.avgResponseTimeMs}ms`,
metrics,
});
return metrics;
} catch (error) {
const endTime = performance.now();
this.results.push({
testName: "API 스트레스 및 네트워크 시뮬레이션",
status: "failed",
duration: endTime - startTime,
details: `오류 발생: ${error}`,
});
throw error;
}
}
/**
* 🎯 모든 스트레스 테스트 실행
*/
static async runAllStressTests() {
console.log("🎯 스트레스 테스트 스위트 시작");
console.log("⚠️ 시스템에 높은 부하를 가할 예정입니다...\n");
const overallStart = performance.now();
this.results = []; // 결과 초기화
try {
// 1. 대량 데이터 처리
console.log("=".repeat(60));
await this.testMassiveDataProcessing();
// 2. 타입 오염 및 손상
console.log("=".repeat(60));
await this.testTypeCorruption();
// 3. 동시 작업 및 경합 상태
console.log("=".repeat(60));
await this.testConcurrentOperations();
// 4. 메모리 부하
console.log("=".repeat(60));
await this.testMemoryStress();
// 5. API 스트레스
console.log("=".repeat(60));
await this.testAPIStress();
const overallEnd = performance.now();
const totalDuration = overallEnd - overallStart;
// 결과 분석
const passedTests = this.results.filter((r) => r.status === "passed").length;
const failedTests = this.results.filter((r) => r.status === "failed").length;
const warningTests = this.results.filter((r) => r.status === "warning").length;
console.log("\n" + "=".repeat(60));
console.log("🎉 스트레스 테스트 완료!");
console.log("=".repeat(60));
console.log(`📊 총 테스트: ${this.results.length}`);
console.log(`✅ 통과: ${passedTests}`);
console.log(`❌ 실패: ${failedTests}`);
console.log(`⚠️ 경고: ${warningTests}`);
console.log(`⏱️ 총 소요시간: ${Math.round(totalDuration)}ms`);
console.log("");
// 개별 테스트 결과 출력
this.results.forEach((result, index) => {
const statusIcon = result.status === "passed" ? "✅" : result.status === "failed" ? "❌" : "⚠️";
console.log(`${statusIcon} ${index + 1}. ${result.testName}`);
console.log(` └─ ${result.details} (${Math.round(result.duration)}ms)`);
});
return {
success: failedTests === 0,
totalTests: this.results.length,
passedTests,
failedTests,
warningTests,
totalDuration,
results: this.results,
recommendation: this.generateRecommendations(),
};
} catch (error) {
console.error("❌ 스트레스 테스트 실행 중 치명적 오류:", error);
return {
success: false,
error: String(error),
results: this.results,
};
}
}
/**
* 📋 테스트 결과 기반 권장사항 생성
*/
private static generateRecommendations(): string[] {
const recommendations: string[] = [];
this.results.forEach((result) => {
if (result.status === "failed") {
recommendations.push(`🔧 ${result.testName}: 실패 원인을 분석하고 타입 시스템을 강화하세요.`);
}
if (result.status === "warning") {
recommendations.push(`⚠️ ${result.testName}: 잠재적 문제가 감지되었습니다. 모니터링을 강화하세요.`);
}
if (result.metrics) {
// 성능 기반 권장사항
if (result.metrics.operationsPerSecond && result.metrics.operationsPerSecond < 1000) {
recommendations.push(
`${result.testName}: 성능 최적화를 고려하세요 (${result.metrics.operationsPerSecond} ops/sec).`,
);
}
if (result.metrics.memoryDeltaMB && result.metrics.memoryDeltaMB > 50) {
recommendations.push(
`🧠 ${result.testName}: 메모리 사용량 최적화를 권장합니다 (${result.metrics.memoryDeltaMB}MB 증가).`,
);
}
if (result.metrics.successRate && result.metrics.successRate < 99) {
recommendations.push(
`🎯 ${result.testName}: 성공률 개선이 필요합니다 (${result.metrics.successRate.toFixed(2)}%).`,
);
}
}
});
if (recommendations.length === 0) {
recommendations.push("🎉 모든 스트레스 테스트를 성공적으로 통과했습니다! 타입 시스템이 매우 견고합니다.");
}
return recommendations;
}
/**
* 📊 테스트 결과 반환 (외부에서 접근 가능)
*/
static getResults() {
return this.results;
}
}
export default StressTestSuite;