sales-3

이지예·2026년 9월 17일

개발

목록 보기
25/58

series: 멀티클라우드 견적 자동화 시스템

스펙 없이는 견적이 성립하지 않는다

이 프로젝트의 핵심 기능은 "4vCPU / 16GB 이상인 인스턴스 중 가장 싼 것을 찾는다"입니다. 그런데 Azure Retail Prices API는 가격만 주고 스펙을 주지 않습니다.

{
  "armSkuName": "Standard_D4s_v5",
  "armRegionName": "koreacentral",
  "retailPrice": 0.192,
  "unitOfMeasure": "1 Hour"
}

vCPU도, 메모리도 없습니다. 스펙이 없으면 매칭 자체가 불가능하니, 이건 부가 기능이 아니라 없으면 프로젝트가 성립하지 않는 문제였습니다.

접근: SKU 이름을 정규식으로 분해한다

다행히 Azure의 VM SKU 이름에는 규칙이 있습니다. Standard_D4s_v5, Standard_E8-4ds_v5 같은 이름에서 시리즈(D, E, F...)와 코어 수를 읽어낼 수 있고, 시리즈마다 vCPU당 메모리 비율이 고정되어 있습니다.

private static final Pattern SKU_PATTERN =
        Pattern.compile("^Standard_([A-Z]+)(\\d+)(?:-(\\d+))?([a-z]*)(?:_v(\\d+))?$");

private static final Map<String, Integer> MEMORY_PER_VCPU = Map.of(
        "A", 2,
        "D", 4,
        "DC", 4,
        "E", 8,
        "EC", 8,
        "F", 2,
        "L", 8
);

이 비율표로 Standard_D4s_v5를 분해하면 시리즈 D, 코어 4 → 메모리 4 × 4GB = 16GB가 나옵니다.

함정 1: 제약 코어 SKU는 메모리가 줄지 않는다

Azure에는 "제약 코어(constrained core)"라는 SKU가 있습니다. Standard_E8-4ds_v5처럼 숫자가 두 번 나오는 경우인데, 처음엔 이걸 그냥 "8코어인데 뭔가 접미사가 붙은 것"으로 오해하고 무시했습니다.

실제 의미는 다릅니다. E8-4는 "메모리·스토리지 대역폭은 8코어 기준을 그대로 쓰되, 실제로 과금되는 vCPU는 4개로 제한한다"는 뜻입니다. 즉 vCPU는 4로 줄지만 메모리는 8코어 기준(64GB)을 그대로 유지합니다.

Matcher matcher = SKU_PATTERN.matcher(armSkuName.trim());
String series = matcher.group(1);
int baseVcpu = Integer.parseInt(matcher.group(2));            // 8
Integer constrainedVcpu = matcher.group(3) == null             // 4
        ? null : Integer.parseInt(matcher.group(3));

Integer memoryPerVcpu = memoryPerVcpu(series, suffix);
int vcpu = constrainedVcpu != null ? constrainedVcpu : baseVcpu;  // 4 (과금 기준)
int memoryGb = baseVcpu * memoryPerVcpu;                          // 8 × 8GB = 64GB (기본 기준 유지)

이걸 놓치면 E8-4ds_v5의 메모리를 32GB(4×8)로 절반 계산하게 되고, "16GB 이상" 조건으로 검색했을 때 이 SKU가 후보에서 빠지거나, 반대로 실제보다 싼 것으로 잘못 매칭됩니다. 단위 테스트로 이 케이스를 고정해뒀습니다.

함정 2: 비율이 불규칙한 시리즈는 아예 제외한다

GPU(N), HPC(H), 초대형 메모리(M) 시리즈는 vCPU당 메모리 비율이 시리즈 안에서도 모델마다 다릅니다. 이런 시리즈까지 억지로 비율표에 넣으면, 특정 모델에서는 맞고 다른 모델에서는 틀리는 상황이 생깁니다.

"틀린 값을 내느니 모른다고 답하는 게 낫다"는 기준을 세우고, 비율이 일정하지 않은 시리즈는 매칭 대상에서 아예 제외했습니다.

if (memoryPerVcpu == null || baseVcpu <= 0) {
    return Optional.empty();
}

resolve()가 Optional.empty()를 반환하면 해당 SKU는 캐시에 저장되지 않고, 매칭 후보에도 오르지 않습니다. 견적 자체가 실패하는 게 아니라, 그 SKU가 "해당 없음"으로 조용히 빠지는 것뿐입니다.

이 방식의 한계는 솔직하게 남긴다

이 파서는 결국 추정(inference)입니다. Azure가 공식적으로 vCPU·메모리를 내려주는 게 아니라, 이름 규칙을 역산한 것이므로 마이크로소프트가 향후 시리즈 이름 규칙을 바꾸면 깨질 수 있습니다.

그래서 두 가지를 했습니다.

  1. AzureSkuSpecResolver 클래스 하나에 이 로직을 전부 격리했습니다. 비율표가 틀렸다는 게 확인되면 이 파일 하나만 고치면 됩니다.
  2. 제약 코어·제외 시리즈를 포함한 파서 단위 테스트 5건으로 회귀를 방지했습니다.
@Test
void constrainedCoreSkuKeepsBaseMemory() {
    InstanceSpec spec = resolver.resolve("Standard_E8-4ds_v5").orElseThrow();
    assertThat(spec.vcpu()).isEqualTo(4);
    assertThat(spec.memoryGb()).isEqualTo(64); // 8코어 기준 메모리 유지
}

@Test
void gpuSeriesIsExcluded() {
    assertThat(resolver.resolve("Standard_NC24ads_A100_v4")).isEmpty();
}

실제 서비스에 쓰려면 여기서 나온 값을 Azure VM 사이즈 공식 문서와 대조 검증하는 과정이 한 번 더 필요합니다. 이건 로컬 연동 테스트 단계에서 별도로 확인할 항목으로 남겨뒀습니다.

다음 편에서는 이렇게 수집한 가격 데이터를 어떻게 캐싱하고, 벤더 하나가 실패해도 나머지가 멀쩡하게 동작하도록 격리했는지를 다룹니다.

profile
github: @dlwldP

0개의 댓글