801 lines
28 KiB
TypeScript
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;
|