728x90

1. 도입: 왜 지금 이 얘기를 꺼내는가

질문 하나 던지고 시작하자. 오늘 당신 프로젝트의 의존성 하나가 털렸다고 치자. 공격자가 OpenAI, Anthropic, Gemini API 키를 빼갈 수 있을까?

이 질문의 답은 어떤 LLM 프로바이더를 쓰느냐, 코드가 얼마나 견고하냐에 달려 있지 않다. 대부분의 팀이 한 번도 진지하게 고민 안 한 딱 하나의 아키텍처 결정에 달려 있다. 바로 "앱이 돌아가는 동안 프로바이더 API 키가 실제로 어디에 존재하느냐"다.

요즘 실무에서 LLM API 도입이 폭발적으로 늘고 있다. 사내 챗봇, RAG 파이프라인, 코드 리뷰 봇까지. 그런데 대부분은 OPENAI_API_KEY를 환경변수에 박고 SDK 초기화해서 끝낸다. 나도 처음엔 그랬다. 문제는 이 방식이 공급망 공격(supply chain attack)에 통째로 노출된다는 점이다.

원문(Dev.to, Hadil Ben Abdallah)에서는 2026년 3월 LiteLLM 공급망 사고를 예로 든다. 이 사고가 주목받은 건 LiteLLM이 유독 취약해서가 아니라, AI 인프라가 이제 얼마나 값진 공격 타깃이 됐는지, 그리고 털린 의존성 하나가 얼마나 빠르게 고가치 크레덴셜에 도달하는지를 보여줬기 때문이다. 이 글에서는 두 가지 게이트웨이 아키텍처를 비교하고, 실제로 돌려볼 수 있는 데모, 그리고 실무에서 시크릿을 어떻게 관리할지까지 정리한다.

2. 핵심: 키가 어디 사느냐가 전부다

모든 LLM 애플리케이션은 결국 같은 일을 한다. 모델 프로바이더에 요청 보내고, API 키로 인증한다. 중요한 건 "API 키를 쓰느냐"가 아니라 "요청하는 순간 그 키가 어디에 존재하느냐"다. 크게 두 가지 패턴이 있다.

패턴 A: 앱이 프로바이더 키를 직접 들고 있다

우리가 제일 익숙한 방식이다. 환경변수에서 키 읽고, SDK 초기화하고, 프로바이더로 직접 쏜다.

import os
from openai import OpenAI

api_key = os.environ["PROVIDER_API_KEY"]
client = OpenAI(api_key=api_key)
response = client.responses.create(...)

구현 빠르고 이해하기 쉽다. 많은 프로젝트에서 충분히 합리적이다. 하지만 핵심은 이거다. 프로바이더 키가 이제 애플리케이션 프로세스 안에 산다. 그 프로세스에서 돌아가는 모든 패키지, 프레임워크, 플러그인, 의존성이 같은 권한으로 실행된다. 그중 하나만 털려도 악성 코드는 당신 앱과 똑같은 환경에서 키를 읽을 수 있다.

패턴 B: 앱이 네트워크 프록시와 대화한다

인증을 앱에서 분리하는 방식이다. 앱은 프로바이더로 직접 쏘지 않고 게이트웨이/프록시로 보낸다. 프록시가 프로바이더 키를 들고 있다가, 자기 프로세스 안에서만 키를 주입해 업스트림 요청을 대신 처리한다.

Application
   │
   ▼
Gateway / Proxy   ← 여기에만 프로바이더 키가 산다
   │
   ▼
LLM Provider

앱 관점에서 흐름은 거의 똑같다. 차이는 앱이 무엇을 안 갖고 있느냐다. 앱은 프로바이더 키 대신 스코프가 제한된 게이트웨이 토큰만 들고 있다. 프록시가 이 토큰을 검증하고, 라우팅/정책을 적용한 뒤, 자기 프로세스 안에서 진짜 프로바이더 키를 붙여 보낸다.

이게 사고의 결과를 바꾼다. 악성 코드가 앱 프로세스에서 실행돼도, 프로바이더 API 키는 애초에 거기 없다. 물론 게이트웨이 토큰이 털리는 것도 보안 사고다. 하지만 게이트웨이 토큰은 좁게 스코프를 걸 수 있고, 중앙에서 즉시 폐기할 수 있고, 앱 재배포 없이 로테이션할 수 있고, 특정 작업만 허용하도록 제한할 수 있다. 프로바이더 키를 갈아끼우는 것과는 회복 난이도가 완전히 다르다.

같은 악성 의존성을 두 아키텍처에 돌려보자

원문의 데모를 재현해보자. 아래는 임포트되는 순간 프로세스 환경변수를 스캔해서 크레덴셜처럼 생긴 걸 출력하는 "악성" 패키지다. 실제로는 네트워크 요청도, 파일 쓰기도 안 하지만 개념 증명엔 충분하다.

# malicious_dep.py
import os

_INTERESTING = ("API_KEY", "SECRET", "TOKEN", "PASSWORD", "PRIVATE_KEY")

def harvest():
    found = {
        k: v for k, v in os.environ.items()
        if any(marker in k.upper() for marker in _INTERESTING)
    }
    for name, value in found.items():
        shown = value[:8] + "..." if len(value) > 12 else value
        print(f"[malicious_dep@import] EXFILTRATED {name}={shown}")

harvest()

이제 시나리오 A. 앱이 프로바이더 키를 직접 환경변수에 들고 있는 경우다.

$ export PROVIDER_API_KEY="sk-provider-abc123xyz"
$ python -c "import malicious_dep"
[malicious_dep@import] EXFILTRATED PROVIDER_API_KEY=sk-provi...3xyz

별 대단한 짓을 한 게 아니다. 인증 우회도, 메모리 손상 익스플로잇도, 다른 서비스 침투도 없다. 그냥 이미 자기가 실행되는 프로세스 안에 있던 데이터를 읽었을 뿐이다. 공격자 입장에선 그걸로 충분하다.

이번엔 시나리오 B. 앱은 게이트웨이 토큰만 들고 있다.

$ unset PROVIDER_API_KEY
$ export GATEWAY_TOKEN="gw-scoped-read-only-789"
$ python -c "import malicious_dep"
[malicious_dep@import] EXFILTRATED GATEWAY_TOKEN=gw-scope...-789

주목할 건 무엇이 안 나왔는지다. 프로바이더 API 키가 없다. 애초에 앱 프로세스에 존재한 적이 없으니까. 앱은 여전히 모델 응답을 정상적으로 받지만, 프로바이더 인증은 프록시 안에서 일어난다.

여기서 "그럼 프록시가 문제를 해결했네"라고 결론 내리면 안 된다. 의존성은 여전히 크레덴셜을 하나 훔쳤다. 게이트웨이 토큰은 진짜고, 공격자가 이걸로 프록시를 통해 요청을 날릴 수도 있다. 핵심은 "뭐가 샜냐"가 아니라 "샌 게 뭘 할 수 있고, 얼마나 빨리 복구되냐"다. 프로바이더 키가 새면 프로바이더 콘솔 들어가서 키 재발급하고 전 서비스 재배포해야 한다. 게이트웨이 토큰은 프록시에서 한 줄로 폐기하고 새 토큰 발급하면 끝이다.

3. 실무 관점: 도입 시 고려사항과 흔한 함정

키 노출 경로별 위험도 비교

실무에서 키가 새는 경로는 생각보다 다양하다. 경로별로 정리하면 이렇다.

  • 앱 프로세스 환경변수: 위에서 봤듯 같은 프로세스의 모든 의존성이 읽는다. 가장 흔하고 가장 넓은 공격면.
  • CI/CD 파이프라인 변수: GitHub Actions Secrets, GitLab CI Variables 등. 로그에 실수로 echo $OPENAI_API_KEY 찍으면 그대로 노출된다. PR에서 돌아가는 워크플로에 시크릿 노출되면 포크에서 탈취 가능.
  • 컨테이너 런타임: docker inspect/proc/1/environ로 환경변수 다 보인다. 이미지 레이어에 ENV OPENAI_API_KEY=... 박아넣으면 이미지 pull 가능한 누구나 본다.
  • .env 파일 커밋: .gitignore에 안 넣고 커밋했다가 GitHub 스크래핑 봇한테 몇 분 만에 털리는 클래식.

공통점은 "키가 앱 프로세스가 도달 가능한 위치에 평문으로 존재"한다는 것이다. 패턴 B(프록시)는 이 문제를 프록시 프로세스 하나로 격리시켜 폭발 반경(blast radius)을 줄인다.

흔한 함정 1: 컨테이너 환경변수는 다 보인다

많은 사람이 컨테이너 안에 환경변수로 넣으면 "격리됐다"고 착각한다. 아니다. 같은 호스트에서 docker 접근 권한만 있으면 이렇게 보인다.

$ docker inspect my-llm-app --format '{{range .Config.Env}}{{println .}}{{end}}'
PATH=/usr/local/bin:/usr/local/sbin
PROVIDER_API_KEY=sk-provider-abc123xyz
LANG=C.UTF-8

또 이미지 빌드 단계에서 ENV로 박으면 레이어 히스토리에 영구히 남는다. docker history로 확인 가능하다. 빌드 시점 시크릿은 반드시 --secret 마운트(BuildKit)를 쓰거나 런타임 주입해야 한다.

흔한 함정 2: Vault 붙였는데 결국 환경변수로 다시 흘린다

Vault를 도입해놓고 앱 시작 스크립트에서 이렇게 하는 경우가 진짜 많다.

export PROVIDER_API_KEY=$(vault kv get -field=api_key secret/llm)
python app.py

이러면 Vault를 쓰든 말든 결국 키가 앱 프로세스 환경변수로 다시 들어온다. 의존성 공격 관점에선 아무것도 안 바뀐 것이다. Vault의 진짜 가치는 중앙 관리, 감사 로그, 자동 로테이션, 동적 시크릿이지, "환경변수에 넣기 전 잠깐 안전"이 아니다.

흔한 함정 3: AWS Secrets Manager 권한 에러

ECS/EKS에서 Secrets Manager 붙이다 보면 십중팔구 이 에러를 만난다.

ResourceInitializationError: unable to pull secrets or registry auth:
execution resource retrieval failed: unable to retrieve secret from asm:
service call has been retried 1 time(s):
AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/ecsTaskExecutionRole/...
is not authorized to perform: secretsmanager:GetSecretValue
on resource: arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:llm/openai-key-AbCdEf
because no identity-based policy allows the secretsmanager:GetSecretValue action

범인은 대부분 Task Execution Rolesecretsmanager:GetSecretValue 권한이 없어서다. 그리고 KMS 커스텀 키로 암호화한 시크릿이면 kms:Decrypt도 추가로 필요하다. 이걸 놓쳐서 한참 헤매는 사람 많다. 최소 권한 정책 예시는 이렇다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:llm/openai-key-*"
    },
    {
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
  ]
}

안전한 시크릿 관리 옵션 비교

  • HashiCorp Vault: 동적 시크릿, 자동 로테이션, 세밀한 정책. 자체 운영 부담이 크다. Vault Agent나 CSI Provider로 파일 마운트하면 환경변수보다 낫다(단, 파일도 프로세스가 읽으면 의존성이 읽음).
  • AWS Secrets Manager: AWS 생태계면 통합이 편하다. 자동 로테이션 지원. IAM 권한 설정이 함정.
  • SOPS: 암호화된 시크릿을 Git에 커밋해서 GitOps로 관리. KMS/age로 암호화. 배포 시점 복호화. 인프라 부담이 적어 소규모 팀에 좋다.
  • LLM 프록시 게이트웨이(패턴 B): LiteLLM Proxy, 자체 프록시 등. 근본적으로 폭발 반경을 줄이는 유일한 방법. 다만 프록시 자체가 새 SPOF가 되고 운영 포인트가 하나 늘어난다.

정직하게 말하면, 어느 것도 만능이 아니다. 프록시를 써도 게이트웨이 토큰은 여전히 프로세스 안에 있다. 관건은 "완전 제거"가 아니라 "털렸을 때 피해를 얼마나 좁히고, 얼마나 빨리 복구하느냐"다.

SOPS로 키를 Git에 안전하게 넣는 실전 예시

소규모 팀이라면 프록시 세우기 전에 SOPS만 붙여도 평문 커밋 사고는 막는다.

$ cat secrets.yaml
openai_api_key: sk-provider-abc123xyz

$ sops --encrypt --age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8j \
    secrets.yaml > secrets.enc.yaml

$ cat secrets.enc.yaml
openai_api_key: ENC[AES256_GCM,data:xJk2...,iv:...,tag:...,type:str]
sops:
    age:
        - recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8j
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            ...
            -----END AGE ENCRYPTED FILE-----

이제 secrets.enc.yaml은 안심하고 커밋해도 된다. 배포 시점에 복호화한다.

$ export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
$ sops --decrypt secrets.enc.yaml
openai_api_key: sk-provider-abc123xyz

4. 정리: 한 줄 요약과 선택 기준

한 줄 요약: LLM API 키 보안의 90%는 "키가 앱 프로세스가 읽을 수 있는 곳에 있느냐"로 결정된다. 프록시로 프로바이더 키를 앱 밖으로 빼내면 의존성이 털려도 폭발 반경이 줄어든다.

  • 혼자/소규모, 빠른 프로토타입: 최소한 SOPS나 Secrets Manager로 평문 커밋·환경변수 하드코딩부터 막아라.
  • 여러 앱이 같은 프로바이더 키 공유, 감사·로테이션 필요: 프록시 게이트웨이(패턴 B)를 진지하게 검토하라. 앱은 스코프 토큰만 들고, 폐기·로테이션을 중앙화한다.
  • 규제/보안 요구가 높은 조직: Vault 동적 시크릿 + 프록시 조합. 운영 부담은 크지만 회복 속도와 감사 추적이 최고다.

핵심은 마인드셋 전환이다. "우리 코드는 안전하다"가 아니라 "우리 의존성 중 하나가 이미 털렸다고 가정하면, 그때 무슨 크레덴셜이 손에 잡히나?"를 물어야 한다. 이 질문에 답이 안 나온다면, 오늘 docker inspect 한 번 돌려보는 것부터 시작하자.

참고 자료

728x90
728x90

지난주에 사내 코드 리뷰 자동화 PoC 돌리다가 또 한 번 깨달은 게 있다. 클로드든 GPT든 외부 API로 보내는 순간, 보안팀에서 "그 코드 어디로 나갑니까"라는 질문이 날아온다는 거다. 사내 레포지토리 코드를 그대로 미국 회사 서버로 토큰화해서 던지는 걸, 규제 산업에선 절대 통과 못 시킨다. 그래서 로컬 LLM을 계속 만지작거리는데, 솔직히 그동안은 다 실망스러웠다. 7B, 13B 모델들은 "데모용"이지 실무용이 아니었으니까.

그런데 Quesma 블로그의 Piotr Migdał이 쓴 "Qwen 3.6 27B is the sweet spot for local development" 글이 HN 프론트에 올라온 걸 보고, 이건 좀 진지하게 봐야겠다 싶었다. 정리해본다.

왜 지금 로컬 LLM인가 — 비용·보안·레이턴시 트레이드오프

로컬 LLM을 고민하게 되는 시점은 보통 셋 중 하나다.

  • 비용: 원문에서도 지적하듯, 지금 프론티어 모델들은 "massive subsidy(대규모 보조금)" 위에서 돌아간다. 월 100달러 내면 토큰 가치로는 수천 달러어치를 쓴다. 이 할인이 영원할 거라 가정하고 파이프라인을 설계하면, 가격 정책 바뀌는 날 CI 비용이 폭발한다. CI에서 PR마다 LLM 코드 리뷰를 돌리면 토큰 소모가 생각보다 빠르게 누적된다.
  • 보안: 사내 코드, 의료 데이터, 미공개 데이터를 외부로 안 보내야 하는 경우. 원문 저자도 "미국이나 중국과 내 깊은 비밀, 의료 데이터를 공유하는 게 불편할 때"라고 직설적으로 쓴다.
  • 레이턴시·오프라인: 네트워크 왕복 없이 로컬에서 즉시 응답. 오프라인 환경, 폐쇄망 개발 환경.

핵심은, 로컬 모델은 "뺏기지 않는다"는 점이다. 원문에서 언급한 Claude Fable 5가 내려간 것처럼, 의존하던 모델이 어느 날 사라지면 워크플로우 전체가 망가진다. 로컬 가중치는 한번 받아두면 내 디스크에 남는다.

Qwen 3.6 27B 아키텍처 해부 — Dense가 MoE보다 나은 상황

Qwen 3.6은 두 가지 변형으로 나온다. 이게 선택의 핵심이다.

  • Qwen 3.6 35B A3B — Mixture-of-Experts(MoE). 전체 35B 파라미터지만 추론 시엔 일부 전문가(A3B = active 3B 추정)만 활성화. 그래서 빠르다.
  • Qwen 3.6 27B — Dense 모델. 모든 파라미터를 매번 쓴다. 느리지만 더 강력하다. 원문 저자가 추천하는 쪽이다.

비유하자면 MoE는 "회의에 필요한 전문가만 호출하는 컨설팅 회사"고, Dense는 "전원이 항상 모든 회의에 들어오는 작은 정예팀"이다. 전자는 빠르고 효율적이지만, 가끔 엉뚱한 전문가가 호출되거나 지시를 놓친다. 후자는 느리지만 일관되게 깊다.

원문의 실제 일화가 이 차이를 잘 보여준다. OpenCode에서 "pnpm으로 헥사고날 지뢰찾기 만들어줘"라고 시켰을 때:

  • 27B (Dense): 단일 프롬프트로 한 번에 성공. 제대로 된 Node 패키지로 만들어냈다.
  • 35B A3B (MoE): 더 빨랐지만, "패키지로 만들라"는 지시를 무시하고 그냥 단일 index.html에 박아넣었다.

여기서 실무 교훈. 제약 조건 준수(instruction following)가 중요한 태스크일수록 Dense가 유리하다. CI에서 "이 컨벤션 지켜서 리팩토링해", "이 디렉토리 구조로 생성해" 같은 까다로운 지시를 던질 거면, 속도를 좀 포기하더라도 27B가 안전하다. 반면 빠른 초안 생성, 채팅형 보조에는 35B A3B가 낫다.

벤치마크로 보면 (원문의 Artificial Analysis 인용):

Gemma 4 31B        : 29  (≈ 2024 말, o1 / Claude 3.5 Sonnet 급)
Qwen3.6-35B-A3B    : 32  (≈ 2025 초, o3 / Claude 4 Sonnet 급)
Qwen3.6-27B        : 37  (≈ 2025 중, GPT-5 / Claude Sonnet 4.5 급)
DeepSeek-V4-Flash  : 40  (≈ 2025 말, GPT-5.2 / Claude Opus 4.5 급)

주의: 이 점수와 "≈ 급" 비교는 원문 기준이며, 미래 시점 모델명이 섞여 있다(원문 작성일이 2026년). 그대로 인용한 거지 내가 검증한 수치는 아니다. 다만 27B가 같은 로컬 디폴트로 많이 쓰이는 Gemma 4 31B를 벤치마크·여론 양쪽에서 큰 차이로 앞선다는 게 원문의 주장이다.

하드웨어 요구사항과 추론 런타임 — 실제로 돌려보기

원문 저자는 llama.cpp를 추천한다. Ollama는 윤리적 이유로 권하지 않는다고 명시했는데, 이 부분은 저자 개인 입장이니 각자 판단하면 된다. 다만 기술적으로 llama.cpp는 직접적이고 오픈소스이며, Apple Silicon 전용인 mlx-lm보다도 더 빨랐다는 게 원문 측정 결과다.

먼저 모델 받아서 서버 띄우기. 8-bit 양자화에 멀티토큰 예측(MTP) 지원 버전을 쓴다:

llama-server -hf unsloth/Qwen3.6-27B-MTP-GGUF:Q8_0 \
  --spec-type draft-mtp -ngl 999 -fa on -c 65536 --port 8080

각 옵션 의미:

  • -hf unsloth/Qwen3.6-27B-MTP-GGUF:Q8_0 — Hugging Face에서 8-bit 양자화 모델을 받는다. 다음 실행부터는 캐시 재사용.
  • --spec-type draft-mtp — 빠른 보조 모델로 다음 토큰을 미리 예측(speculative decoding). 속도 향상.
  • -ngl 999 — 모든 레이어를 GPU에 올린다.
  • -fa on — flash attention 켜기.
  • -c 65536 — 컨텍스트 64k 토큰. Qwen 3.6 27B의 네이티브 컨텍스트는 256k라 더 늘릴 수도 있다.
  • --port 8080 — 포트 고정. 다른 설정에서 재사용하니까.

서버 뜨면 http://127.0.0.1:8080에서 바로 채팅 가능하다. 성능 측정치(MacBook M5 Max 128GB 기준, 원문):

모델                          tok/s    RAM
Qwen3.6-27B (llama.cpp)       18       41 GB
Qwen3.6-27B (llama.cpp+MTP)   32       42 GB
Qwen3.6-35B-A3B (llama.cpp+MTP) 105     45 GB

여기서 짚을 점 두 가지.

첫째, MTP가 27B에서 거의 2배 속도(18→32 tok/s)를 낸다. MTP 빼고 돌리면 답답할 수 있으니 꼭 켜라.

둘째, 35B A3B가 27B보다 3배 빠르다(105 vs 32). 그래도 원문 저자는 27B를 택한다. "1/3만큼 코드를 생성하더라도 더 높은 품질을 원한다"는 이유다. 이 선택은 팀 성향에 달렸다 — 빠른 반복이 중요하냐, 한 방의 정확도가 중요하냐.

Nvidia RTX 카드라면 양자화를 더 공격적으로 해야 한다. HN 댓글의 gfosco는 RTX 5090에서 Q6_K 양자화 + Q4_0 KV로 123k 컨텍스트에서 50 tok/s를 LM Studio로 뽑았다고 한다(약 28/32GB VRAM 사용). 즉 27B는 48GB Apple Silicon 공유 메모리 안에서 돌고, 소비자급 GPU에서도 양자화만 잘 맞추면 충분히 돌아간다.

OpenCode 같은 에이전트에 물리려면 ~/.config/opencode/opencode.jsonc에:

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "llama": {
      "name": "llama.cpp (local)",
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "baseURL": "http://127.0.0.1:8080/v1",
        "apiKey": "local"
      },
      "models": {
        "qwen3.6-27b": { "name": "Qwen3.6-27B Q8 +MTP" }
      }
    }
  },
  "model": "llama/qwen3.6-27b"
}

핵심은 llama-server가 OpenAI 호환 엔드포인트(/v1)를 노출한다는 거다. 그래서 baseURL만 로컬로 돌리면 기존 OpenAI SDK 쓰는 코드/툴 대부분이 그대로 붙는다.

실무 관점 — 흔한 함정과 트레이드오프

함정 1: VRAM 초과로 레이어가 CPU로 떨어진다

-ngl 999로 전부 GPU에 올리려 했는데 VRAM이 모자라면, llama.cpp가 일부 레이어를 CPU로 오프로드한다. 그럼 속도가 절벽처럼 떨어진다. 보통 이런 로그가 뜬다:

llama_model_load: error loading model: unable to allocate CUDA0 buffer
ggml_backend_cuda_buffer_type_alloc_buffer: allocating 8192.00 MiB on device 0: cudaMalloc failed: out of memory

이때 해법은 (1) 더 공격적인 양자화로 내리거나(Q8_0 → Q6_K → Q4_K_M), (2) -ngl 값을 실제 올릴 수 있는 레이어 수로 줄이거나, (3) 컨텍스트 -c를 줄여서 KV 캐시 메모리를 절약하는 것이다. 컨텍스트 64k가 메모리를 꽤 먹으니, 코드 리뷰처럼 짧은 입력이면 -c 16384 정도로도 충분할 때가 많다.

함정 2: MTP 옵션 누락 또는 draft 모델 미지원

--spec-type draft-mtp를 줬는데 받은 GGUF가 MTP를 지원 안 하는 버전이면 speculative decoding이 동작 안 하거나 경고가 뜬다. 반드시 모델 이름에 -MTP-가 들어간 변형(Qwen3.6-27B-MTP-GGUF)을 받아야 한다. 일반 양자화 받아놓고 MTP 옵션만 켜면 속도 이득(18→32)을 못 본다.

함정 3: 양자화 수준과 품질의 함정

원문 기준 8-bit(Q8_0)는 품질 손실이 거의 없다. 하지만 RTX 카드에서 메모리 맞추려고 Q4 이하로 내리면 코드 생성 품질이 눈에 띄게 떨어질 수 있다. 특히 긴 컨텍스트에서 더 그렇다. "왜 우리 로컬 모델은 클로드보다 한참 멍청하지?"의 범인이 사실은 모델이 아니라 과도한 양자화인 경우가 많다. 벤치마크 점수도 8-bit 기준이라는 걸 기억하자.

함정 4: CI에 통합할 때 동시성

llama-server 한 개 인스턴스에 PR 빌드 여러 개가 동시에 붙으면 큐가 밀린다. Dense 27B는 그렇잖아도 32 tok/s 수준이라, 동시 요청이 쌓이면 CI 잡이 타임아웃 난다. 이럴 땐 처리량(throughput) 최적화가 강한 vLLM으로 갈아타거나, GPU를 여러 장 두고 인스턴스를 늘리는 걸 고려해야 한다. 원문은 단일 개발자 워크플로우 중심이라 이 부분은 다루지 않으니, 팀 단위 CI 통합이라면 처리량 벤치마크를 별도로 잡아봐야 한다(공식 문서 확인 필요).

트레이드오프 정리

  • 품질 우선, 지시 준수 중요 → Qwen 3.6 27B Dense + Q8_0 + MTP
  • 속도 우선, 채팅/초안 → Qwen 3.6 35B A3B (MoE)
  • 소비자 GPU(VRAM 24~32GB) → Q6_K 정도로 타협, KV는 Q4_0
  • 팀 CI 처리량 필요 → vLLM 계열 검토

정리 — 누가 언제 써야 하나

한 줄 요약: Qwen 3.6 27B는 "외부로 코드 못 보내는 환경에서, 단일 개발자/소규모 팀이 품질 좋은 로컬 코딩 어시스턴트를 쓰고 싶을 때" 현실적인 첫 선택지다.

이런 사람에게 권한다:

  • 48GB Apple Silicon이나 24GB+ VRAM GPU를 가진 개발자 — 바로 돌려볼 수 있다.
  • 보안/규제 때문에 클라우드 API를 못 쓰는 백엔드·인프라 팀의 PoC 단계.
  • 외부 API 비용 보조금이 사라질 위험에 대비해 로컬 백업 워크플로우를 두려는 팀.

반대로 지금 당장은 보류할 사람: 수백 명이 동시에 붙는 대규모 CI 처리량이 필요하거나, 프론티어 최상위 모델 품질이 반드시 필요한 경우. 그땐 원문이 언급한 GLM 5.2 같은 더 큰 오픈웨이트(단일 맥북/5090으론 안 돌고 회사 예산급 하드웨어 필요)를 보거나, 그냥 클라우드 API가 답이다.

개인적으로는, "내가 통제하고 뺏기지 않는 모델"이라는 가치 하나만으로도 한 번 세팅해둘 값어치는 충분하다고 본다. 명령어 몇 줄이면 끝나니, 주말에 5090이든 맥북이든 한번 띄워보고 사내 코드 한 조각 리뷰시켜보는 걸 추천한다.

참고 자료

※ 본문의 벤치마크 점수, tok/s, RAM 수치는 모두 원문(Quesma 블로그) 측정값을 인용한 것이며, 미래 시점 모델명이 섞여 있으니 실제 도입 전 최신 공식 문서로 재확인 바란다.

728x90

+ Recent posts