AWS 기술 블로그

GS Neotek의 Claude Code on Amazon Bedrock 도입기: LLM Gateway 없는 경량화 사용자 통제 방안

GS Neotek은 AWS 프리미어 파트너로, 다수의 고객에게 클라우드 마이그레이션·현대화·운영 서비스를 제공하며 생성형 AI 도입을 지원하고 있습니다. 이 글은 여러 조직에 코딩 어시스턴트 도입을 지원하며 축적한 경험을 바탕으로, LLM Gateway 없이도 사용자별 비용을 통제하는 경량 아키텍처를 정리한 것입니다.

Amazon Bedrock을 통해 Claude Code와 같은 코딩 어시스턴트를 사용하기 위한 기본 조건은 의외로 단순합니다. 모델 호출을 위한 인증 수단(IAM 자격 증명)을 마련하고, 해당 자격 증명에 Bedrock 모델 호출 권한을 부여하기만 하면 API를 통한 Claude 사용이 가능합니다.

그러나 조직 규모가 커지고 내부 통제 방침이 더해지면 이야기가 달라집니다. 개인이 자유롭게 Claude를 사용하도록 두는 것을 넘어, 조직 차원의 추가적인 통제 수단이 필요해지기 때문입니다. 이 글에서는 조직 차원에서 코딩 어시스턴트를 도입할 때 고려해야 할 사항을 정리하고, 각 의사결정에 따라 어떤 아키텍처를 선택하는 것이 바람직한지 살펴봅니다. 특히 LLM Gateway 없이도 사용자별 비용을 산출하고 예산 초과 사용자를 차단하는 경량 아키텍처를 중심으로 다룹니다.

코딩 어시스턴트 도입 시 고려사항

일반적으로 Claude와 같은 Model as a Service(MaaS)를 조직에 도입할 때 고민하게 되는 영역은 다음과 같습니다. 이 외에도 다양한 요소가 있지만, 본 글에서는 아래 항목을 우선적으로 다룹니다.

영역 고려사항
AWS 환경 구성 Single Account vs Multi Account
Public Endpoint 호출 vs Private Endpoint(VPC Endpoint) 호출
사용자 규모 대비 TPM(Tokens Per Minute) / RPM(Requests Per Minute) 한도
사용자 통제 로직 Bedrock API 호출용 자격 증명 발급 및 관리
사용자별 사용량 확인
사용자별 사용량 제어
사용자별·모델별 접근 제어
사용 이력 관리 Guardrails 적용
요청/응답 로그 저장
사용자별 사용 이력 관리
운영 대시보드 MAU, DAU
모델별 사용 비용
TPM / RPM
프로젝트/조직/개인별 비용 관리
Guardrails 탐지 횟수
호출 지연(Latency), 응답 오류율(Error Rate)
Admin 기능 특정 사용자(예산 초과자 등) 차단
할당량 조정
호출 가능 모델 및 사용량 조정

LLM Gateway는 언제 필요한가

이러한 고려사항은 아키텍처 의사결정에 직접적인 영향을 미칩니다. 예를 들어 다음과 같은 요구사항을 모두 가진 조직이라면 LLM Gateway 도입이 합리적인 선택입니다.

  • 데이터 유출 방지, 시맨틱 캐싱 등을 목적으로 사용자의 요청을 검사하거나 변경해야 함
  • 사용자의 모든 호출에 대한 실시간 통제가 필요함
  • 토큰 사용량 대비 성과 관리가 필요함

이러한 조직은 LLM Gateway를 도입하여 사용자 단위 Virtual Key 발급으로 비용을 분리하고, 예산 설정을 통해 사용자를 실시간으로 차단하며, OTLP(OpenTelemetry Protocol) 기반 데이터 전송을 통해 상용/오픈소스 대시보드로 가시성을 확보할 수 있습니다.

그러나 모든 조직이 LLM Gateway를 구성할 만큼 복잡한 요구사항을 가지는 것은 아닙니다. LLM Gateway는 개발 공수, 유지보수 부담, 그리고 상시 구동되는 인프라 비용을 수반합니다. 조직 규모에 비해 과한 선택이 될 수 있습니다.

도입 형태에 따라 추가로 고려할 비용과 위험도 있습니다. 상용 솔루션은 사용자 수나 처리량에 비례하는 라이선스 비용이 발생하며, 오픈소스 솔루션은 라이선스 비용이 없는 대신 취약점 대응 책임을 조직이 부담합니다. 실제로 대표적인 오픈소스 LLM Gateway인 LiteLLM에서는 인증 우회, SQL 인젝션, 원격 코드 실행 등 심각도가 높은 취약점이 지속적으로 보고되고 있습니다. LLM Gateway는 조직의 모든 모델 호출이 통과하는 단일 경로이자 API 키와 자격 증명이 집중되는 지점이므로, 이러한 취약점은 곧 전체 AI 워크로드의 위험으로 이어집니다. 따라서 오픈소스를 채택하는 경우 버전 추적과 신속한 패치 적용을 담당할 인력과 프로세스를 함께 계획해야 합니다.

그렇다면 사용자 단위 비용 계산과 차단 로직은 반드시 LLM Gateway를 통해서만 가능할까요? 그렇지 않습니다.

경량 비용 통제 아키텍처

핵심 아이디어는 이렇습니다. 프록시 형태의 LLM Gateway로 모든 요청을 실시간으로 가로채는 대신, Bedrock이 남기는 모델 호출 로그를 주기적으로 집계하여 예산 초과 사용자를 준실시간(near-real-time)으로 사후 차단하는 것입니다.

즉, 예산 초과 시점부터 차단 반영까지 수 분 정도의 지연을 허용할 수 있다면(로그 적재 지연과 배치 주기를 포함), LLM Gateway 인프라를 프로비저닝하는 것보다 훨씬 저비용으로, 그리고 빠르게 코딩 어시스턴트 도입을 가속화할 수 있습니다.

이 방식은 약 200명 규모의 중견 조직뿐만 아니라, 본격적인 LLM Gateway 도입에 앞서 빠르게 Amazon Bedrock 기반 코딩 어시스턴트를 시범 도입하려는 조직에서도 충분히 고려할 만한 옵션입니다.

주의: 이 아키텍처는 로그 기반 사후 집계 방식이므로 요청 단위의 실시간 차단(pre-request enforcement)은 제공하지 않습니다. Bedrock 모델 호출 로그는 S3/CloudWatch에 적재되기까지 수 분의 지연이 발생할 수 있으며, 집계 배치 주기까지 더해지면 “예산을 초과한 순간부터 차단이 반영되기까지” 짧은 공백이 존재합니다. 이 공백을 감수할 수 있는지가 아키텍처 선택의 핵심 기준입니다.

제약 사항: 이 아키텍처는 조직의 모든 Claude 호출이 Bedrock Runtime을 경유한다는 전제에서 동작합니다. Bedrock Model Invocation Logging은 bedrock-runtime 데이터 플레인 호출(InvokeModel, InvokeModelWithResponseStream, Converse, ConverseStream)만 기록하며, Claude Code에서는 CLAUDE_CODE_USE_BEDROCK=1 설정이 이 경로에 해당합니다. 같은 Claude 모델이라도 다른 경로(Anthropic API 직접 호출 등)로 호출하면 이 로그에 동일한 형태로 남지 않으므로, 비용 집계와 차단이 함께 무력화됩니다. 따라서 조직의 호출 경로를 bedrock-runtime으로 표준화하는 것이 이 아키텍처의 선행 요건입니다. mantle endpoint 유형으로 모델을 호출하는 경우 별도의 로깅 구성이 필요합니다.

최소한의 비용 통제를 목표로 하는 아키텍처는 아래와 같습니다. 지금부터 각 구성 요소가 어떻게 비용 통제를 구현하는지 세부적으로 살펴보겠습니다.

[그림 1] 경량 비용 통제 아키텍처 전체 구성

1. 사용자별 비용 계산 방식

Amazon Bedrock은 Model Invocation Logging 기능을 통해 모델 호출 로그를 S3 또는 CloudWatch Logs에 저장할 수 있습니다. 이 로그에는 사용자를 식별할 수 있는 identity.arn이 포함됩니다.

  • AWS Organizations + AWS IAM Identity Center(SSO) 환경을 사용하는 경우, 로그에는 SSO가 생성한 예약 역할(reserved role) 세션의 ARN(AWSReservedSSO_…)이 남습니다.
  • Single Account + IAM User 환경을 사용하는 경우, 호출한 IAM User의 ARN이 남습니다.

또한 로그에는 모델 호출과 소비된 토큰을 식별할 수 있는 다음 항목이 포함됩니다. 이 값들을 조합하면 어느 사용자가 어떤 모델을 얼마나 사용했는지 계산할 수 있으며, 모델 단가표에 따라 사용 금액을 산출할 수 있습니다.

  • modelId
  • input.inputTokenCount
  • input.cacheReadInputTokenCount
  • input.cacheWriteInputTokenCount
  • output.outputTokenCount

참고: 캐시 관련 토큰 필드(cacheReadInputTokenCount, cacheWriteInputTokenCount)는 로그 상위 레벨에 항상 존재하는 것은 아닙니다. 이 경우 output.outputBodyJson 내부 message_start 이벤트의 usage 필드(cache_read_input_tokens, cache_creation_input_tokens)와 교차 검증하여 보완하는 것이 안전합니다. 아래 예시 로그에서 두 위치의 값이 일치하는 것을 확인할 수 있습니다.

실제 로그 예시는 다음과 같습니다.

{
  "timestamp": "2026-07-02T04:32:07Z",
  "accountId": "***MASKED_ACCOUNT_ID***",
  "region": "ap-northeast-2",
  "requestId": "***MASKED_REQUEST_ID***",
  "operation": "InvokeModelWithResponseStream",
  "modelId": "arn:aws:bedrock:ap-northeast-2:***MASKED_ACCOUNT_ID***:inference-profile/global.anthropic.claude-opus-4-8",
  "input": {
    "inputContentType": "application/json",
    "inputBodyS3Path": "s3://***MASKED_BUCKET_NAME***/AWSLogs/***MASKED_ACCOUNT_ID***/BedrockModelInvocationLogs/ap-northeast-2/2026/07/02/04/data/***MASKED_REQUEST_ID***_input.json.gz",
    "inputTokenCount": 2,
    "cacheReadInputTokenCount": 117856,
    "cacheWriteInputTokenCount": 2479
  },
  "output": {
    "outputContentType": "application/json",
    "outputBodyJson": [
      {
        "type": "message_start",
        "message": {
          "model": "claude-opus-4-8",
          "id": "***MASKED_MESSAGE_ID***",
          "type": "message",
          "role": "assistant",
          "content": [],
          "stop_reason": null,
          "stop_sequence": null,
          "stop_details": null,
          "usage": {
            "input_tokens": 2,
            "cache_creation_input_tokens": 2479,
            "cache_read_input_tokens": 117856,
            "cache_creation": {
              "ephemeral_5m_input_tokens": 2479,
              "ephemeral_1h_input_tokens": 0
            },
            "output_tokens": 1,
            "service_tier": "standard"
          }
        }
      }
    ]
  }
}

단가표는 백만(1,000,000) 토큰당 USD 기준입니다. 캐시 쓰기 단가는 TTL에 따라 배수가 달라집니다. 기본 입력가 대비 5분 TTL은 1.25배, 1시간 TTL은 2.0배이며, 캐시 읽기는 0.1배입니다. 예를 들어 입력 단가가 $3.00인 Sonnet의 경우 캐시 쓰기는 5분 TTL $3.75, 1시간 TTL $6.00, 캐시 읽기 $0.30이 됩니다. 아래는 5분 TTL을 가정한 계산 로직입니다.

# cost = Σ(토큰 수 × 해당 단가) / 1,000,000
# 단가는 "백만 토큰당 USD" 기준입니다.
# 아래는 예시 단가표이며, 실제 값은 사용하는 모델과 리전의 최신 요금표를 확인하세요.
PRICING_EXAMPLE = {
    "sonnet-4-6": {"input": 3.0, "output": 15.0, "cache_write_5m": 3.75, "cache_write_1h": 6.00, "cache_read": 0.30}
}

cost = (
    input.inputTokenCount            * 3.0
    + input.cacheReadInputTokenCount   * 0.30
    + input.cacheWriteInputTokenCount  * 3.75  # 5분 TTL 가정. 1시간 TTL 사용 시 6.00 적용
    + output.outputTokenCount          * 15.0
) / 1_000_000

참고: 위 단가는 설명을 위한 예시입니다. Bedrock의 모델별·리전별 요금은 변동될 수 있으므로, 실제 구현 시 Amazon Bedrock 요금 페이지의 최신 값을 확인하시기 바랍니다.

주의: 로그 상위 레벨의 input.cacheWriteInputTokenCount는 5분 TTL과 1시간 TTL 캐시 쓰기를 합산한 단일 값이므로, 이 필드만으로는 두 단가를 구분할 수 없습니다. 구분이 필요하면 output.outputBodyJson 내부 message_start 이벤트의 cache_creation.ephemeral_5m_input_tokens와 ephemeral_1h_input_tokens를 사용해야 합니다. 예시에서는 5분 TTL을 적용하는 것으로 작성하였습니다.

2. 전체 사용 로그 조회

위 계산은 단일 API 호출에 대한 비용 산출 로직입니다. 실제로 한 달 동안 사용자가 사용한 Bedrock 비용을 구하려면, 비용 통제 기간 전체에 대해 사용된 모든 모델의 단가를 적용하여 사용자별로 집계해야 합니다.

이를 위해 S3에 적재된 로그 전체를 조회하고 사용자별로 집계하는 데 Amazon Athena를 활용합니다. 아래는 요청 단위 중복을 제거하고 사용자·모델별 토큰을 집계하기 위한 샘플 쿼리입니다.

SELECT
    identity.arn                                    AS identity_arn,
    modelId,
    input.inputTokenCount                           AS input_tokens,
    COALESCE(input.cacheReadInputTokenCount,  0)    AS cache_read_tokens,
    COALESCE(input.cacheWriteInputTokenCount, 0)    AS cache_write_tokens,
    output.outputTokenCount                         AS output_tokens,
    ROW_NUMBER() OVER (
        PARTITION BY requestid
        ORDER BY (
            input.inputTokenCount +
            output.outputTokenCount +
            COALESCE(input.cacheReadInputTokenCount,  0) +
            COALESCE(input.cacheWriteInputTokenCount, 0)
        ) DESC
    ) AS rn
FROM {ATHENA_TABLE}
WHERE (
        (year = '{year}'      AND month = '{month}')
     OR (year = '{prev_year}' AND month = '{prev_month}' AND day = '{last_day_prev}')
     OR (year = '{next_year}' AND month = '{next_month}' AND day = '01')
    )

파티션 프루닝(year / month / day)으로 스캔 대상을 최소화하고, UTC와 로컬 타임존 경계에서 발생하는 로그를 놓치지 않도록 전월 말일과 익월 1일 파티션을 함께 포함한 뒤 timestamp 필터로 정확한 월을 다시 걸러내는 방식입니다. 스트리밍 응답이 여러 로그로 분할될 수 있어 requestid 기준으로 대표 레코드 하나를 선택합니다.

3. 사용자별 비용 산출

Athena로 조회한 결과를 사용자별로 집계하여 비용을 계산하는 로직을 Python 함수로 표현하면 다음과 같습니다.

def calculate_costs(rows, is_long_context=False):
    """Athena 결과 rows → 사용자별 비용 dict"""
    user_costs = defaultdict(lambda: {
        "cost": 0.0, "calls": 0,
        "standard_cost": 0.0, "long_context_cost": 0.0,
        "model_costs": defaultdict(float),
        "input_token": defaultdict(float), "cache_read_tokens": defaultdict(float),
        "cache_write_tokens": defaultdict(float), "output_tokens": defaultdict(float)
    })

    for row in rows:
        arn       = row.get("identity_arn", "unknown")
        base_tier = get_model_tier(row.get("modelId", ""))
        use_long  = is_long_context and base_tier in LONG_CONTEXT_MODELS
        p         = PRICING.get(f"{base_tier}-long" if use_long else base_tier, PRICING["sonnet-4-6"])

        i  = int(row.get("input_tokens",       0) or 0)
        cr = int(row.get("cache_read_tokens",  0) or 0)
        cw = int(row.get("cache_write_tokens", 0) or 0)
        o  = int(row.get("output_tokens",      0) or 0)
        calls = int(row.get("invocations",     0) or 0)

        cost = (i * p["input"] + cr * p["cache_read"] + cw * p["cache_write"] + o * p["output"]) / 1_000_000

        user_costs[arn]["cost"]  += cost
        user_costs[arn]["calls"] += calls
        user_costs[arn]["model_costs"][base_tier] += cost
        if use_long:
            user_costs[arn]["long_context_cost"] += cost
        else:
            user_costs[arn]["standard_cost"] += cost
        user_costs[arn]["input_token"][base_tier]        += i
        user_costs[arn]["cache_read_tokens"][base_tier]  += cr
        user_costs[arn]["cache_write_tokens"][base_tier] += cw
        user_costs[arn]["output_tokens"][base_tier]      += o

    return dict(user_costs)

일부 모델은 긴 컨텍스트(long context) 구간에서 다른 단가가 적용되므로, is_long_context 플래그와 LONG_CONTEXT_MODELS 집합을 통해 해당 단가표({base_tier}-long)를 선택하도록 구성했습니다.

[그림 2] Athena Query 실행 결과

4. 사용자 비용 테이블 저장

위 로직을 AWS Lambda에서 실행할 때, 사용자별로 어떤 모델을 얼마나 사용했고 총 비용이 얼마인지를 저장하는 Amazon DynamoDB 테이블을 구성합니다. 외부에서 조회가 필요할 경우 DynamoDB 콘솔에서 직접 확인하거나, 별도의 대시보드가 필요하다면 Amazon API Gateway를 앞단에 두어 조회 API를 제공하는 방식을 권장합니다.

대시보드나 외부 클라이언트가 Amazon DynamoDB에 직접 접근하도록 자격 증명을 배포하는 방식은 권한 관리와 보안 측면에서 권장하지 않습니다. Amazon API Gateway(+ Lambda 또는 DynamoDB 직접 통합)를 통해 접근 경로를 통제하는 것이 바람직합니다.

def save_to_dynamodb(year_month, user_costs):
    """user_costs dict를 DynamoDB에 저장 (month + identity_arn 복합 키)

    - DynamoDB(boto3) 제약으로 float → Decimal 변환이 필요합니다.
    - 당월 데이터는 put_item으로 덮어써(idempotent) 재실행 시에도 안전합니다.
    """
    saved = 0
    for arn, info in user_costs.items():
        item = {
            "month":             _serializer.serialize(year_month),
            "identity_arn":      _serializer.serialize(arn),
            "cost":              _serializer.serialize(Decimal(str(round(info["cost"], 6)))),
            "calls":             _serializer.serialize(info["calls"]),
            "standard_cost":     _serializer.serialize(Decimal(str(round(info["standard_cost"], 6)))),
            "long_context_cost": _serializer.serialize(Decimal(str(round(info["long_context_cost"], 6)))),
            "model_costs":       _serializer.serialize(
                {k: Decimal(str(round(v, 6))) for k, v in info["model_costs"].items()}
            ),
        }
        dynamodb_client.put_item(TableName=DYNAMODB_TABLE, Item=item)
        saved += 1

    print(f"[INFO] Saved {saved} items to DynamoDB table '{DYNAMODB_TABLE}' for month={year_month}")
    return saved

month(파티션 키)와 identity_arn(정렬 키)의 복합 키 구조를 사용하면 특정 월의 전체 사용자 비용을 쉽게 쿼리할 수 있고, 배치가 재실행되어도 put_item으로 멱등하게 갱신됩니다.

5. 예산 초과 사용자 탐지

예산을 초과한 사용자를 탐지하는 절차는 다음과 같습니다.

  1. 앞서 저장한 사용량 테이블에서 해당 월에 사용 이력이 있는 사용자와 비용을 불러옵니다.
  2. 사용자별 예산을 정의한 별도의 예산 테이블에서 각 사용자에게 할당된 예산을 조회합니다.
  3. 두 테이블의 값을 비교하여 사용 금액이 할당 예산을 초과한 사용자를 차단 대상자로 분류합니다. (개인별 예산이 없으면 조직 기본값 BUDGET_LIMIT 적용)
user_budget_table = get_user_budget()

for arn, info in user_costs.items():
    if info["cost"] > user_budget_table.get(arn, {}).get("budget_limit", BUDGET_LIMIT):
        session_name = extract_session_name(arn)
        role_name    = extract_role_name(arn)
        email        = lookup_email(session_name)
        if not session_name or not role_name:
            continue

        user_entry = {
            "arn":          arn,
            "session_name": session_name,
            "role_name":    role_name,
            "email":        email,
            "cost":         round(info["cost"], 2),
            "model_costs":  dict(info.get("model_costs", {})),
        }
        all_blocked.append(user_entry)
        if session_name in previously_blocked_sessions:
            already_blocked.append(user_entry)
        else:
            newly_blocked.append(user_entry)

previously_blocked_sessions와 비교하여 신규 차단자(newly_blocked)와 기존 차단자(already_blocked)를 구분하면, 신규 차단자에게만 알림을 발송하는 등 후속 처리를 효율적으로 할 수 있습니다.

6. 차단 방안

차단을 구현하는 방식은 계정 구조에 따라 달라집니다. AWS Organizations + IAM Identity Center 환경을 예로 들면, Claude Code 사용자가 속한 Permission Set에 초과 사용자를 담을 전용 관리형 정책(Deny 정책)을 미리 연결(attach)해 둡니다. 이 정책은 기본적으로 아무도 지정하지 않은 빈 상태(placeholder)로 두고, 초과가 탐지될 때마다 해당 사용자의 식별자(예: SSO 역할/세션 조건)를 이 정책에 추가하여 Bedrock 호출을 거부하도록 구성할 수 있습니다.

[그림 3] IAM Identity Center 기반 접근 제어 및 예산 초과 사용자 차단 구조

아래는 관리형 정책의 최신 버전을 갱신하는 로직입니다. IAM 관리형 정책은 버전을 최대 5개까지만 보유할 수 있으므로, 새 버전을 생성하기 전에 기본(default)이 아닌 이전 버전을 정리합니다.

def _update_managed_policy(policy_name, policy_doc):
    account_id = get_account_id()
    policy_arn = f"arn:aws:iam::{account_id}:policy/{policy_name}"
    try:
        # 관리형 정책은 최대 5개 버전 제한 → 새 버전 생성 전 non-default 버전 정리
        versions = iam_client.list_policy_versions(PolicyArn=policy_arn)["Versions"]
        for v in versions:
            if not v["IsDefaultVersion"]:
                iam_client.delete_policy_version(PolicyArn=policy_arn, VersionId=v["VersionId"])
        iam_client.create_policy_version(
            PolicyArn=policy_arn,
            PolicyDocument=json.dumps(policy_doc),
            SetAsDefault=True,
        )
        print(f"[INFO] Updated managed policy {policy_name}")
    except iam_client.exceptions.NoSuchEntityException:
        print(f"[ERROR] Policy {policy_name} does not exist.")
    except Exception as e:
        print(f"[ERROR] Failed to update managed policy {policy_name}: {e}")

팁: Deny 정책은 명시적 거부(explicit deny)가 항상 우선한다는 IAM 평가 규칙을 활용합니다. bedrock:InvokeModel, bedrock:InvokeModelWithResponseStream, bedrock:Converse, bedrock:ConverseStream 등 모델 호출 액션을 거부 대상으로 지정하면 초과 사용자의 Claude 호출이 즉시 차단됩니다. 대규모 조직에서는 개별 관리형 정책 대신 SCP(Service Control Policy)나 세션 태그 기반 조건을 활용하는 방식도 검토할 수 있습니다.

예산을 초과한 사용자는 DynamoDB 테이블에 저장된 개인별 예산을 증액하거나, 전체 예산을 증액하는 방식으로 차단 해제할 수 있습니다. 임의로 customer managed policy에 포함된 사용자를 제외하는 경우에는 Lambda의 다음 배치 주기에 비용 계산을 통해 다시 차단 대상자로 분류되므로 당월 예산을 초과한 사용자는 할당 예산을 증액하는 방식이 바람직합니다.

7. 비용 통제 로직 실행 주기

이 비용 통제 로직은 AWS Lambda로 실행되므로 Amazon EventBridge 스케줄러 등을 통해 주기적으로 트리거해야 합니다. 사용자 규모와 허용 가능한 지연을 고려하여 최소 1분에서 최대 15분 사이로 설정할 수 있습니다.

주기를 짧게 잡을수록 예산 초과 반영이 빨라지지만, Bedrock 로그 적재 지연이 존재하므로 1분 단위로 설정하더라도 완전한 실시간 차단은 아니라는 점을 유의해야 합니다. 실무에서는 5분 주기 정도가 비용(Athena 스캔 및 Lambda 실행)과 반영 속도 사이의 합리적인 균형점이 되는 경우가 많습니다.

정리

LLM Gateway는 강력한 도구이지만, 모든 조직에 필요한 것은 아닙니다. 요청 단위 실시간 차단이 필수가 아니고, 수 분의 반영 지연을 허용할 수 있는 조직이라면 다음의 서버리스 구성만으로 사용자별 비용 통제를 충분히 달성할 수 있습니다.

  • Bedrock Model Invocation Logging → S3 로그 적재
  • Athena → 사용 로그 집계
  • Lambda → 비용 계산 및 초과 탐지
  • DynamoDB → 사용량/예산 저장
  • IAM Deny 정책(또는 SCP) → 초과 사용자 차단
  • EventBridge → 주기적 실행

[그림 4] 비용 통제 상세 아키텍처

이 구성은 완전 관리형(managed) 서버리스 서비스만으로 이루어지므로 다음과 같은 이점을 가집니다.

  • 고정비 최소화: 상시 구동되는 서버가 없어 실제 실행한 배치와 스캔한 데이터에 대해서만 비용이 발생합니다.
  • 빠른 구축: 별도의 애플리케이션을 개발할 필요 없이 로깅 활성화와 Lambda 배포만으로 짧은 기간에 적용할 수 있습니다.
  • 유지보수 부담 없음: 패치, 스케일링, 가용성 확보를 AWS가 담당하므로 게이트웨이 운영을 위한 전담 인력이 필요하지 않습니다.
  • 확장성: 사용자와 호출량이 늘어나도 Athena와 Lambda가 자동으로 확장되어 아키텍처 변경 없이 대응할 수 있습니다.
  • 보안 표면 최소화: 직접 운영하는 인바운드 엔드포인트가 없고 IAM으로 접근을 통제하므로, 별도로 취약점을 추적하고 패치해야 할 구성 요소가 크게 줄어듭니다.

이러한 특성 덕분에 수십~수백 명 규모의 조직에서 빠르게 코딩 어시스턴트를 도입하기에 적합합니다. 향후 실시간 통제나 정교한 라우팅이 필요해지면 그때 LLM Gateway로 전환하는 단계적 접근도 가능합니다.

GS Neotek은 이 글에서 설명한 경량 비용 통제 아키텍처를 여러 고객 환경에 반복 적용할 수 있는 오퍼링으로 정형화했습니다. 조직의 계정 구조(Single/Multi Account), 네트워크 요구사항, 사용자 규모에 맞춰 로그 집계·비용 산출·차단 로직을 구성하고, 필요 시 LLM Gateway 기반 실시간 통제로 확장하는 단계적 도입까지 지원합니다. Amazon Bedrock 기반 코딩 어시스턴트를 빠르고 안전하게 도입하려는 조직이라면 검증된 패턴 위에서 시작할 수 있습니다.

참고 자료

문승현

문승현 솔루션즈 아키텍트는 GS Neotek에서 어플리케이션 운영, AI/ML 등 다양한 영역에서 고객들의 비즈니스 문제를 해결하고 구현하는 데 도움을 드리고 있습니다. 생성형 AI와 코딩 어시스턴트의 조직 도입 및 비용 최적화를 중점적으로 지원하고 있습니다.

Byeongseung Jeon

Byeongseung Jeon

전병승 파트너 솔루션즈 아키텍트는 AWS 파트너사들의 기술적 역량 강화와 성장을 지원하고 있습니다. 클라우드 아키텍처 설계에서 탁월한 전문성을 바탕으로, 파트너사들이 혁신적인 솔루션을 개발하고 성공적으로 구현할 수 있도록 적극적인 지원을 제공하고 있습니다.

jaeseoyu

jaeseoyu

유재서 솔루션즈 아키텍트는 SW 개발 경험과 모범 사례를 바탕으로 고객의 요구사항을 도출하고, 다양한 워크로드를 안정적이고 효율적으로 구축할 수 있도록 기술적인 지원을 제공하고 있습니다. 최적의 클라우드 아키텍처를 설계하는 역할을 수행하며, 특히 엔터프라이즈의 클라우드 전환과 생성형 AI 도입 여정을 함께하고 있습니다.