반응형

 

 

Ncloud 리소스를 Terraform으로 변환하는 과정에서 리소스 참조 로직의 문제점을 발견하고, 이를 개선하기 위해 참조 캐싱을 적용한 리팩토링 과정을 공유하고자 합니다.

문제 발견

Terraform 변환 작업 중 대규모 리소스를 처리할 때 성능 저하가 발생하는 문제를 발견했습니다. 원인을 분석해보니 리소스 간 참조를 처리하는 로직에 비효율이 있었습니다.

// 기존 코드
export const replaceReferences = (
    properties: { [key: string]: any },
    resourceNameMap: ReferenceMap,
): { [key: string]: any } => {
    const result = { ...properties };  // 얕은 복사로 시작
    
    for (const [key, value] of Object.entries(result)) {
        if (typeof value === 'string') {
            result[key] = resolveReference(value);
        } else if (Array.isArray(value)) {
            result[key] = value.map((item) =>
                typeof item === 'string'
                    ? resolveReference(item)
                    : replaceReferences({ value: item }, resourceNameMap).value,
            );
        } else if (typeof value === 'object' && value !== null) {
            result[key] = replaceReferences(value, resourceNameMap);
        }
    }

    return result;
};

주요 문제점

  1. 깊은 복사로 인한 메모리 사용량 증가
    • 객체와 배열마다 복사본 생성
    • 대규모 리소스 처리 시 메모리 부담
  2. 재귀 호출의 비효율성
    • 깊은 중첩 구조에서 재귀 깊이 증가
    • 스택 오버플로우 위험성
  3. 중복 작업
    • 동일한 참조 반복 처리
    • 캐싱 메커니즘 부재

해결 방안

1. 참조 캐싱 도입

동일한 참조를 반복 처리하는 문제를 해결하기 위해 캐싱 메커니즘을 도입했습니다.

export class ReferenceReplacer {
    private referenceCache = new Map<string, string>();
    
    constructor(private resourceNameMap: ReferenceMap) {}

    private getCachedReference(value: string): string {
        // 캐시된 참조 확인
        if (this.referenceCache.has(value)) {
            return this.referenceCache.get(value)!;
        }

        // 새로운 참조 생성 및 캐시
        const resolvedReference = this.resolveReference(value);
        this.referenceCache.set(value, resolvedReference);
        return resolvedReference;
    }
}

2. 얕은 복사로 최적화

불필요한 깊은 복사를 피하기 위해 필요한 부분만 변환하는 방식으로 변경했습니다.

replaceReferences(properties: { [key: string]: any }): { [key: string]: any } {
    // 얕은 복사로 시작
    const result = { ...properties };
    
    // 모든 키를 순회하면서 필요한 곳만 변환
    for (const [key, value] of Object.entries(result)) {
        result[key] = this.transformValue(value);
    }
    
    return result;
}

private transformValue(value: any): any {
    // 문자열인 경우 캐시된 참조 확인
    if (typeof value === 'string') {
        return this.getCachedReference(value);
    }
    
    // 배열인 경우 각 요소만 변환 (얕은 복사)
    if (Array.isArray(value)) {
        return value.map(item => this.transformValue(item));
    }
    
    // 객체인 경우 한 레벨만 변환 (얕은 복사)
    if (typeof value === 'object' && value !== null) {
        const transformed = { ...value };
        for (const [k, v] of Object.entries(transformed)) {
            transformed[k] = this.transformValue(v);
        }
        return transformed;
    }
    
    return value;
}

3. 성능 측정 도구 구현

개선 효과를 측정하기 위한 도구를 개발했습니다.

export class ReferenceMetrics {
    private static instance: ReferenceMetrics;
    private replacementTimes: Map<string, number[]> = new Map();
    private cacheHits: number = 0;
    private cacheMisses: number = 0;

    recordCacheHit(): void {
        this.cacheHits++;
    }

    recordCacheMiss(): void {
        this.cacheMisses++;
    }

    getMetrics(): string {
        // 메트릭 보고서 생성 로직
    }
}

개선 효과

1. 성능 향상

  • 단일 참조 처리 속도 82% 향상 (0.11ms → 0.02ms)
  • 캐시 히트율 평균 85% 달성
  • 1000개 이상 리소스 처리 시 전체 처리 시간 60% 단축

2. 메모리 사용량 감소

  • 얕은 복사 사용으로 객체 생성 최소화
  • 대규모 리소스 처리 시 메모리 사용량 약 50% 감소

3. 안정성 향상

  • 재귀 깊이 제한으로 스택 오버플로우 위험 감소
  • 예외 처리 개선으로 오류 회복력 증가

성능 측정 결과

다음은 대규모 클라우드 인프라(약 1,000개 리소스)를 처리할 때의 성능 비교입니다

측정 항목 개선 전 개선 후 개선율

측정 항목 개선 전 개선 후 개선율
참조 처리 시간 0.11ms/참조 0.02ms/참조 82%
전체 처리 시간 2.5초 1초 60%
메모리 사용량 약 180MB 약 90MB 50%
캐시 히트율 0% 85% -

결론

클라우드 리소스 처리 시 참조 해결은 필수적인 작업이지만, 구현 방식에 따라 성능이 크게 달라질 수 있습니다. 캐싱 메커니즘 도입과 불필요한 복사 최소화를 통해 처리 속도와 메모리 효율성을 크게 개선할 수 있었습니다.

이러한 최적화는 특히 대규모 인프라를 다루는 엔터프라이즈 환경에서 중요한 차이를 만들 수 있으며, 작은 성능 개선이라도 누적되면 사용자 경험과 리소스 효율성에 큰 영향을 미치게 됩니다.

반응형