조인기능 최적화

This commit is contained in:
kjs
2025-09-16 16:53:03 +09:00
parent 6a3a7b915d
commit 1d05965a55
8 changed files with 1082 additions and 201 deletions

View File

@@ -1446,9 +1446,12 @@ export class TableManagementService {
};
}
// 조인 전략 결정
// 조인 전략 결정 (테이블 크기 기반)
const strategy =
await entityJoinService.determineJoinStrategy(joinConfigs);
console.log(
`🎯 선택된 조인 전략: ${strategy} (${joinConfigs.length}개 Entity 조인)`
);
// 테이블 컬럼 정보 조회
const columns = await this.getTableColumns(tableName);
@@ -1477,7 +1480,7 @@ export class TableManagementService {
offset,
startTime
);
} else {
} else if (strategy === "cache_lookup") {
// 캐시 룩업 방식
return await this.executeCachedLookup(
tableName,
@@ -1485,6 +1488,18 @@ export class TableManagementService {
options,
startTime
);
} else {
// 하이브리드 방식: 일부는 조인, 일부는 캐시
return await this.executeHybridJoin(
tableName,
joinConfigs,
selectColumns,
whereClause,
orderBy,
options.size,
offset,
startTime
);
}
} catch (error) {
logger.error(`Entity 조인 데이터 조회 실패: ${tableName}`, error);
@@ -1585,7 +1600,7 @@ export class TableManagementService {
try {
// 캐시 데이터 미리 로드
for (const config of joinConfigs) {
await referenceCacheService.preloadReferenceTable(
await referenceCacheService.getCachedReference(
config.referenceTable,
config.referenceColumn,
config.displayColumn
@@ -1766,4 +1781,200 @@ export class TableManagementService {
);
}
}
// ========================================
// 🎯 하이브리드 조인 전략 구현
// ========================================
/**
* 하이브리드 조인 실행: 일부는 조인, 일부는 캐시 룩업
*/
private async executeHybridJoin(
tableName: string,
joinConfigs: EntityJoinConfig[],
selectColumns: string[],
whereClause: string,
orderBy: string,
limit: number,
offset: number,
startTime: number
): Promise<EntityJoinResponse> {
try {
logger.info(`🔀 하이브리드 조인 실행: ${tableName}`);
// 각 조인 설정을 캐시 가능 여부에 따라 분류
const { cacheableJoins, dbJoins } =
await this.categorizeJoins(joinConfigs);
console.log(
`📋 캐시 조인: ${cacheableJoins.length}개, DB 조인: ${dbJoins.length}`
);
// DB 조인이 있는 경우: 조인 쿼리 실행 후 캐시 룩업 적용
if (dbJoins.length > 0) {
return await this.executeJoinThenCache(
tableName,
dbJoins,
cacheableJoins,
selectColumns,
whereClause,
orderBy,
limit,
offset,
startTime
);
}
// 모든 조인이 캐시 가능한 경우: 기본 쿼리 + 캐시 룩업
else {
return await this.executeCachedLookup(
tableName,
cacheableJoins,
{ page: Math.floor(offset / limit) + 1, size: limit, search: {} },
startTime
);
}
} catch (error) {
logger.error("하이브리드 조인 실행 실패", error);
throw error;
}
}
/**
* 조인 설정을 캐시 가능 여부에 따라 분류
*/
private async categorizeJoins(joinConfigs: EntityJoinConfig[]): Promise<{
cacheableJoins: EntityJoinConfig[];
dbJoins: EntityJoinConfig[];
}> {
const cacheableJoins: EntityJoinConfig[] = [];
const dbJoins: EntityJoinConfig[] = [];
for (const config of joinConfigs) {
// 캐시 가능성 확인
const cachedData = await referenceCacheService.getCachedReference(
config.referenceTable,
config.referenceColumn,
config.displayColumn
);
if (cachedData && cachedData.size > 0) {
cacheableJoins.push(config);
console.log(
`📋 캐시 사용: ${config.referenceTable} (${cachedData.size}건)`
);
} else {
dbJoins.push(config);
console.log(`🔗 DB 조인: ${config.referenceTable}`);
}
}
return { cacheableJoins, dbJoins };
}
/**
* DB 조인 실행 후 캐시 룩업 적용
*/
private async executeJoinThenCache(
tableName: string,
dbJoins: EntityJoinConfig[],
cacheableJoins: EntityJoinConfig[],
selectColumns: string[],
whereClause: string,
orderBy: string,
limit: number,
offset: number,
startTime: number
): Promise<EntityJoinResponse> {
// 1. DB 조인 먼저 실행
const joinResult = await this.executeJoinQuery(
tableName,
dbJoins,
selectColumns,
whereClause,
orderBy,
limit,
offset,
startTime
);
// 2. 캐시 가능한 조인들을 결과에 추가 적용
if (cacheableJoins.length > 0) {
const enhancedData = await this.applyCacheLookupToData(
joinResult.data,
cacheableJoins
);
return {
...joinResult,
data: enhancedData,
entityJoinInfo: {
...joinResult.entityJoinInfo!,
strategy: "hybrid",
performance: {
...joinResult.entityJoinInfo!.performance,
cacheHitRate: await this.calculateCacheHitRate(cacheableJoins),
hybridBreakdown: {
dbJoins: dbJoins.length,
cacheJoins: cacheableJoins.length,
},
},
},
};
}
return joinResult;
}
/**
* 데이터에 캐시 룩업 적용
*/
private async applyCacheLookupToData(
data: any[],
cacheableJoins: EntityJoinConfig[]
): Promise<any[]> {
const enhancedData = [...data];
for (const config of cacheableJoins) {
const cachedData = await referenceCacheService.getCachedReference(
config.referenceTable,
config.referenceColumn,
config.displayColumn
);
if (cachedData) {
enhancedData.forEach((row) => {
const keyValue = row[config.sourceColumn];
if (keyValue) {
const lookupValue = cachedData.get(String(keyValue));
if (lookupValue) {
row[config.aliasColumn] = lookupValue;
}
}
});
}
}
return enhancedData;
}
/**
* 캐시 적중률 계산
*/
private async calculateCacheHitRate(
cacheableJoins: EntityJoinConfig[]
): Promise<number> {
if (cacheableJoins.length === 0) return 0;
let totalHitRate = 0;
for (const config of cacheableJoins) {
const hitRate = referenceCacheService.getCacheHitRate(
config.referenceTable,
config.referenceColumn,
config.displayColumn
);
totalHitRate += hitRate;
}
return totalHitRate / cacheableJoins.length;
}
}