sales-2

이지예·2026년 9월 17일

개발

목록 보기
24/58

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

문제: 벤더마다 전혀 다른 API

AWS, Azure, GCP의 가격 조회 API는 인증 방식, 응답 스키마, 페이지네이션 방식이 전부 다릅니다.

인증페이지네이션응답 형태
Azure없음 (완전 공개)NextPageLink가 null이 될 때까지 반복JSON 배열
AWSIAM 자격증명 (SigV4)토큰 기반JSON 문자열의 배열 (이중 인코딩)
GCPAPI Key토큰 기반Core/RAM SKU가 분리된 카탈로그

이 차이를 상위 계층(스케줄러, 캐시 저장 로직)까지 새어 들어오게 하면, 벤더를 하나 추가할 때마다 스케줄러 코드를 건드려야 합니다. 그래서 계약을 하나 정의하고 벤더별 차이는 구현체 안에 가두기로 했습니다.

공통 인터페이스

public interface VendorPriceClient {
    VendorType vendorType();
    List<InstancePrice> fetchPrices(String logicalRegion, OsType os);
}

"논리 리전 + OS를 받아서 InstancePrice 목록을 반환한다"는 계약만 남기고, 나머지는 전부 구현체 책임입니다. 스케줄러는 이 인터페이스만 알면 됩니다.

public record InstancePrice(
        String sku,
        int vcpu,
        int memoryGb,
        String regionCode,
        OsType os,
        BigDecimal hourlyCostUsd
) {}

Azure — 무인증이지만 페이지네이션과 스펙 부재

GET https://prices.azure.com/api/retail/prices
    ?$filter=serviceName eq 'Virtual Machines' and armRegionName eq 'koreacentral'

가장 다루기 쉬운 벤더입니다. 인증이 없어서 RestClient로 바로 호출할 수 있습니다. NextPageLink가 null이 될 때까지 반복 호출하는 것만 신경 쓰면 됩니다.

String url = baseUrl + "?$filter=" + filter;
while (url != null) {
    AzurePriceResponse response = restClient.get().uri(url).retrieve().body(AzurePriceResponse.class);
    prices.addAll(response.items());
    url = response.nextPageLink();
}

문제는 응답에 vCPU·메모리 정보가 없다는 것입니다. armSkuName(Standard_D4s_v5)만 주기 때문에, 이 이름을 파싱해서 스펙을 복원해야 합니다. 이 이야기는 분량이 커서 3편에서 따로 다룹니다.

AWS — 이중 인코딩된 응답과 깊은 중첩 구조

AWS Price List Query API는 SDK(software.amazon.awssdk:pricing)를 쓰면 서명은 자동 처리되지만, 응답 구조가 까다롭습니다.

GetProductsResponse response = pricingClient.getProducts(GetProductsRequest.builder()
        .serviceCode("AmazonEC2")
        .filters(
            Filter.builder().type(FilterType.TERM_MATCH).field("instanceType").value("t3.xlarge").build(),
            Filter.builder().type(FilterType.TERM_MATCH).field("regionCode").value("ap-northeast-2").build(),
            Filter.builder().type(FilterType.TERM_MATCH).field("operatingSystem").value("Linux").build()
        )
        .build());

response.priceList()는 JSON 객체가 아니라 JSON "문자열"의 배열로 옵니다. 한 번 더 파싱해야 하고, 그 안에서도

product.attributes.{vcpu, memory}
terms.OnDemand.*.priceDimensions.*.pricePerUnit.USD

로 깊게 중첩돼 있습니다. terms.OnDemand 아래 키가 고정 ID가 아니라 매번 랜덤한 해시라서, 첫 번째 값을 순회해서 꺼내는 방식으로 처리했습니다. 그리고 단위가 Hrs인 항목만 골라야 스토리지·라이선스 같은 다른 차원의 가격이 섞이지 않습니다.

리전도 주의할 점이 있습니다. Pricing API 엔드포인트 자체는 us-east-1에 고정되어 있습니다. 서울 리전(ap-northeast-2) 가격을 조회할 때도 클라이언트 리전은 us-east-1로 두고, 조회 대상 리전은 필터의 regionCode 값으로 지정해야 합니다. 처음엔 이 둘을 헷갈려서 계속 빈 응답을 받았습니다.

GCP — 단가가 이미 합쳐진 게 아니라 쪼개져 있다

GCP Cloud Billing Catalog API는 AWS·Azure와 근본적으로 다른 구조입니다. t3.xlarge처럼 "머신 타입 하나에 가격 하나"가 아니라, Core SKU와 RAM SKU가 분리되어 있습니다.

services.list                        → Compute Engine 서비스 ID 조회
services/{serviceId}/skus.list       → SKU별 가격 조회 (코어 단가, RAM 단가 각각)

그래서 n2-standard-4 같은 표준 머신 타입 단가를 얻으려면, 같은 패밀리의 코어 단가와 메모리 단가를 각각 수집한 다음 (코어 단가 × vCPU) + (메모리 단가 × GB)로 직접 합성해야 합니다. 이 방식은 정확도가 떨어질 여지가 있어서, GCP는 기본값을 비활성(enabled: false)으로 두고 필요할 때만 켜도록 했습니다.

결과: 스케줄러는 벤더를 모른다

@Component
public class PriceSyncService {
    private final List<VendorPriceClient> priceClients; // Spring이 구현체를 자동 주입

    public SyncReport syncAll() {
        List<SyncOutcome> outcomes = new ArrayList<>();
        for (VendorPriceClient client : priceClients) {
            for (String region : properties.priceSync().regions()) {
                for (OsType os : properties.priceSync().osTypes()) {
                    outcomes.add(syncSafely(client, region, os));
                }
            }
        }
        return new SyncReport(Instant.now(), outcomes);
    }
}

List<VendorPriceClient>를 스프링이 자동으로 주입해주기 때문에, 벤더를 하나 더 추가하고 싶으면 VendorPriceClient 구현체 하나를 새로 만들기만 하면 됩니다. 스케줄러·캐시 저장 로직은 한 줄도 건드리지 않습니다.

다음 편에서는 이 중 가장 까다로웠던 문제 — Azure가 주지 않는 vCPU·메모리를 어떻게 복원했는지를 자세히 다룹니다.

profile
github: @dlwldP

0개의 댓글