728x90

1. 도입: 방문자 14명에 $31, 뭔가 단단히 잘못됐다

사이드 프로젝트 하나쯤 굴려본 사람이라면 이 시나리오가 남 일 같지 않을 거다. Dev.to에 올라온 "My app didn't go viral. My AWS bill did." 글이 딱 그 케이스다. Umami 애널리틱스는 한 달에 방문자 14명을 찍었는데, AWS는 $31을 청구했다. 방문자 한 명당 $2.21. 무료 도구치고는 캘리포니아에서 제일 비싼 학습 도구가 된 셈이다.

여기서 핵심은 결말이다. 그 14명 방문자는 청구서와 아무 상관이 없었다. 진짜 범인은 같은 주말에 글쓴이가 작업하던 전혀 다른 프로젝트(vigil-crest)였고, Bedrock의 과금 구조 때문에 그 비용이 엉뚱한 앱 이름표를 달고 있었던 것뿐이다.

이 글이 인프라 엔지니어한테 와닿는 이유는, "비용이 늘었다"는 단순한 사건이 아니라 대시보드가 거짓말을 하고, AI 비용 추적 도구가 잘못된 모델명을 들이밀고, 과금이 엉뚱한 프로젝트에 귀속되는 복합적인 함정을 정확히 짚었기 때문이다. 실무에서 비용 폭탄 원인을 추적해본 사람은 안다. 진짜 어려운 건 "얼마 나왔냐"가 아니라 "누가, 어디서, 왜 썼냐"를 증거로 못 박는 일이다.

2. 핵심: 왜 작은 트래픽에서도 청구서가 터지는가 — 과금 구조의 비결정성

원문이 잘 짚은 포인트 두 개를 인프라 관점으로 풀어보자.

(1) 트래픽 수와 비용은 직결되지 않는다

Umami 같은 클라이언트 사이드 애널리틱스는 "내 자바스크립트를 실행한 브라우저"만 센다. 봇도, 백엔드 API 호출도, Bedrock 모델 호출도 못 본다. 즉 방문자 수와 실제 과금 사이엔 인과관계가 없는데, 사람은 자꾸 이 둘을 묶어서 본다. 글쓴이 표현대로 "서로 무관한 두 숫자를 엮어 스스로를 겁줬다."

실무에서도 똑같다. CloudWatch 대시보드의 요청 수가 평소랑 같은데 비용만 튀는 경우, 십중팔구 범인은 트래픽이 아니라 데이터 전송, NAT Gateway, 로그 보존, 항상 켜진 인스턴스 같은 "조용한 비용"이다. 댓글에 달린 사례처럼 Cloud Logging 로그 보존만으로 이틀에 $50이 나오기도 한다.

(2) 모델/리소스 과금은 "프로젝트"를 모른다

원문에서 제일 중요한 기술적 통찰이 이거다. Bedrock 과금은 계정 레벨이다. 어떤 함수가 호출했든, 그 모델 비용은 호출한 리소스의 태그를 물려받지 않는다. Application Inference Profile을 따로 설정하고 그걸 호출하지 않는 한, 모든 모델 비용은 "프로젝트 없음" 버킷에 떨어지고, 뭔가가 그걸 떠안게 된다. 글쓴이의 경우 비용 추적 도구가 그걸 멋대로 Clew Directive 탓으로 돌렸다.

비유하자면 회사 공용 법인카드 명세서 같은 거다. 카드 한 장으로 여러 팀이 긁었는데, 명세서엔 "누가 긁었는지"가 안 찍히면 회계가 추정으로 갖다 붙인다. 그 추정이 틀리면 엉뚱한 팀이 누명을 쓴다.

(3) 비용의 "모양"을 읽는 법

원문에서 제일 실무적으로 배울 만한 건 토큰 사용량 분해다. 28M 토큰, 8일 활동, 그중 이틀이 70%를 차지. 그리고 그 모양이:

Cache writes: 4.1M tokens, $15.33 (55%)
Cache reads:  23.8M tokens, $7.14 (26%)
Output:       346K tokens, $5.20
Input:        120K tokens, $0.36

웹 앱이 14명한테 응답하는 비용은 절대 이렇게 안 생겼다. 앞단에 캐시 쓰기가 무겁고, 뒤로 캐시 읽기가 무겁고, 실제 입출력은 거의 없는 모양은 "큰 고정 컨텍스트를 캐싱해두고 매 턴마다 다시 읽는 에이전트"의 지문이다. 댓글에서 다른 엔지니어가 정확히 짚었듯, 이 패턴은 깜짝 놀랄 모델 청구서를 만났을 때 CloudTrail을 한 줄씩 읽기 전에 던질 수 있는 좋은 첫 가설이다.

3. 실무 관점: 조기 감지와 흔한 함정

(1) AWS Budgets로 비용 경보부터 걸어라

가장 먼저 할 일은 청구서가 터지기 전에 알림을 받는 거다. 콘솔에서도 되지만, CLI로 예산을 거는 게 재현 가능해서 좋다. 아래는 월 $10 임계치에 80% 도달 시 이메일 알림을 거는 예시다.

# budget.json
{
  "BudgetName": "monthly-cost-guard",
  "BudgetLimit": { "Amount": "10", "Unit": "USD" },
  "TimeUnit": "MONTHLY",
  "BudgetType": "COST"
}

# notifications.json
[
  {
    "Notification": {
      "NotificationType": "ACTUAL",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 80,
      "ThresholdType": "PERCENTAGE"
    },
    "Subscribers": [
      { "SubscriptionType": "EMAIL", "Address": "you@example.com" }
    ]
  }
]
$ aws budgets create-budget \
    --account-id 123456789012 \
    --budget file://budget.json \
    --notifications-with-subscribers file://notifications.json

# (성공 시 출력 없음 — exit code 0)
$ echo $?
0

여기서 흔한 함정 하나. 권한이 부족하면 이런 에러를 만난다:

An error occurred (AccessDeniedException) when calling the CreateBudget operation:
User: arn:aws:iam::123456789012:user/devops is not authorized to perform:
budgets:CreateBudget on resource: arn:aws:budgets::123456789012:budget/monthly-cost-guard

이 경우 IAM 정책에 budgets:CreateBudget, budgets:ModifyBudget 액션이 필요하다. 그리고 Budgets는 글로벌 서비스라 리전을 us-east-1 기준으로 다루는 게 안전하다(공식 문서 확인 권장).

(2) Cost Explorer로 "무엇이" 쓰는지 분해하기

경보가 울렸으면 다음은 분해다. 글쓴이가 Amazon Q로 했던 일을 CLI로 직접 하면 이렇다. 서비스별 일자별 비용을 뽑아보자.

$ aws ce get-cost-and-usage \
    --time-period Start=2024-05-01,End=2024-06-01 \
    --granularity DAILY \
    --metrics "UnblendedCost" \
    --group-by Type=DIMENSION,Key=SERVICE \
    --query 'ResultsByTime[].{date:TimePeriod.Start, groups:Groups[?Metrics.UnblendedCost.Amount > `0.5`].[Keys[0],Metrics.UnblendedCost.Amount]}'

출력 예시(특정 이틀에 비용이 몰린 모양을 보고 싶을 때):

[
  {
    "date": "2024-05-24",
    "groups": [
      ["Amazon Bedrock", "11.20"],
      ["Amazon Elastic Compute Cloud - Compute", "0.21"]
    ]
  },
  {
    "date": "2024-05-25",
    "groups": [
      ["Amazon Bedrock", "8.40"]
    ]
  }
]

여기서 핵심 교훈. Cost Explorer가 깨끗한 데이터를 줘도, 그 위에 얹는 "이야기"는 틀릴 수 있다. 원문에서 Amazon Q는 정확한 증거(역할명, 인스턴스, 타임스탬프, 모델)를 가져왔지만 세 번이나 잘못된 범인을 지목했다. "앱이 비싸다 → 앱 로그를 봐라" 같은 그럴듯한 서사를 붙인 거다. 데이터는 문제가 아니었고, 데이터 위의 내러티브가 문제였다.

그래서 글쓴이의 룰: "Trust retrieval, verify recall." 도구가 가져온 것(retrieve)은 믿되, 도구가 기억하는 것(recall)은 검증하라. 같은 Q가 Haiku 가격을 실제의 1/4로 잘못 인용하기도 했다. 모델명, 단가 같은 "외운 값"은 항상 코드와 IAM 정책으로 대조해야 한다.

(3) 흔한 함정 모음

  • 비용 추적 도구의 모델명/단가를 믿어버린다. 원문처럼 "Sonnet이 비싸니까 범인이다" 하고 종결하면 틀린다. IAM 정책이 Nova ARN으로만 스코프돼 있으면 Sonnet 호출은 AccessDenied로 막히니, 그 함수는 물리적으로 Sonnet을 청구할 수 없다. 코드와 정책이 최종 증거다.
  • 애널리틱스 방문자 수와 인프라 비용을 엮는다. 둘은 별개 시스템이다.
  • 조용한 비용을 놓친다. 토큰 폭주는 시끄럽고 자기 한정적이다(노트북 닫으면 끝남). 진짜 무서운 건 항상 켜진 t3.micro처럼 시간당 과금되는 리소스다. 싸고 잊혀진 게 표준 비용으로 슬금슬금 쌓인다. 댓글의 로그 보존 비용($50/2일)도 같은 부류다.
  • 리전을 잘못 골라서 데이터 전송/빌드 비용을 더 낸다. 댓글 사례처럼 US Central이 아닌 곳에서 빌드하다 추가 과금이 붙기도 한다.

(4) 대안과 트레이드오프

모델 비용을 프로젝트별로 추적하고 싶다면 Application Inference Profile을 설정해서 태그가 붙는 프로파일을 호출하는 방식이 있다. 다만 설정 부담이 늘고, 호출 코드를 프로파일 ARN으로 바꿔야 한다. 작은 사이드 프로젝트라면 차라리 프로젝트별로 AWS 계정을 분리(또는 Organizations + 계정 분리)하는 게 비용 귀속을 가장 깔끔하게 만든다. 비용이 계정 경계에서 자연스럽게 갈리기 때문이다.

4. 사이드 프로젝트용 비용 방어 — 배포 전 체크리스트

원문 교훈을 인프라 체크리스트로 압축하면 이렇다.

  • AWS Budgets 경보부터 건다. 임계치 50%/80%/100% 세 단계. 청구서보다 알림이 먼저 와야 한다.
  • "항상 켜진" 리소스 목록을 안다. EC2, NAT Gateway, RDS, ElastiCache는 트래픽 0이어도 돈을 먹는다. 안 쓰면 끈다.
  • 로그 보존 기간과 verbosity를 점검한다. 디버깅용으로 켠 verbose 로그가 청구서의 주범이 되는 게 흔하다.
  • IAM 정책으로 모델/리소스를 스코프한다. 비싼 모델 ARN 접근을 아예 막아두면, 사고로라도 비싼 호출이 안 나간다(원문의 Nova-only 스코핑).
  • 비용이 어느 프로젝트에 귀속되는지 미리 정한다. 계정 분리 또는 Inference Profile/태깅 전략. "프로젝트 없음" 버킷을 줄여라.
  • 비용 도구의 결론을 코드/정책/CloudTrail로 교차 검증한다. 첫 번째 확신에 찬 문장을 받아들이지 마라.

5. 정리: 한 줄 요약과 적용 대상

한 줄 요약: AWS 비용 폭탄의 진짜 어려움은 "얼마"가 아니라 "누가 왜 썼는지를 증거로 못 박는 것"이다. 대시보드도, 비용 추적 AI도 깨끗한 데이터 위에 틀린 이야기를 얹을 수 있으니, 가져온 것은 믿되 기억한 것은 검증하라.

누가 언제 써야 하나:

  • 사이드 프로젝트/스타트업을 AWS에 굴리는 개인 개발자 — 배포 전에 Budgets 경보와 항상 켜진 리소스 목록부터 챙겨라.
  • Bedrock 등 모델 API를 여러 프로젝트에서 공유하는 팀 — 지금 비용 귀속 전략(계정 분리 or Inference Profile)을 정해라. "프로젝트 없음" 버킷은 사고의 온상이다.
  • 이미 청구서가 튄 사람 — Cost Explorer로 비용의 모양(캐시 쓰기/읽기/입출력 비율)부터 보고, 그게 앱 트래픽 모양인지 에이전트 모양인지 판단해라.

마지막으로 원문 글쓴이의 결론이 인상적이다. 범인은 바이럴이 아니라 "EC2로 만든 트렌치코트를 입고 다음 프로젝트를 만들던 나 자신"이었다고. 비용 추적의 끝은 대개 거울 속에 있다.

참고 자료

728x90
728x90

Hacker News에 Bunny DNS가 무료화됐다는 글이 올라왔다. "또 무료 떡밥이냐" 싶을 수 있는데, DNS는 인프라 비용에서 의외로 뒤통수 맞기 쉬운 영역이라 한 번 짚고 갈 만하다. 5년차 굴린 입장에서 실무 관점으로 풀어본다.

1. 도입: 왜 지금 화제이고 어떤 문제를 푸는지

DNS 비용은 평소엔 존재감이 없다. 월 청구서 보면 CDN, 컴퓨트, 스토리지가 압도적이고 DNS는 몇 달러 수준이라 신경도 안 쓴다. 문제는 트래픽이 튀거나 공격받을 때 터진다.

AWS Route53을 예로 들면 쿼리당 과금이다. 공식 요금 기준으로 표준 쿼리는 100만 건당 $0.40 (첫 10억 쿼리까지). 평소엔 푼돈인데, 봇이나 DDoS성 DNS 증폭 쿼리가 들어오면 이 숫자가 갑자기 뛴다. 실제로 잘못 설정한 헬스체크나 짧은 TTL 때문에 쿼리량이 폭증해서 청구서 보고 놀라는 케이스가 있다.

Bunny가 이번에 한 건 DNS 쿼리 과금 자체를 없앤 것이다. 원문 기준 계정당 500개 도메인까지 무료 호스팅, 쿼리 제한·건당 과금 없음, smart record와 헬스 모니터링도 포함. 단 bunny.net 공통 정책인 월 $1 최소 사용료는 적용된다(DNS 자체엔 사용량 과금이 없을 뿐).

요금 구조를 거칠게 정리하면 이렇다. (수치는 각 사 공식 요금이 시점에 따라 바뀌니 직접 확인 필요)

서비스호스팅 비용쿼리 과금비고
Route53호스팅 존당 월 $0.50쿼리당 과금 있음존·쿼리 많아지면 누적
Cloudflare무료 플랜 존재기본 무료고급 기능은 유료 플랜
Bunny DNS500도메인까지 무료없음(쿼리 과금 폐지)계정당 월 $1 최소 사용료

핵심은 "쿼리 폭증 = 비용 폭증"이라는 불안 요소를 제거했다는 점이다. 비용 예측 가능성은 인프라 운영에서 생각보다 큰 가치다.

2. 핵심: Anycast와 스마트 라우팅, 어떻게 동작하는가

Bunny DNS의 출발점은 자기네 CDN을 위한 내부 라우팅 엔진이었다고 한다. 원문 표현대로 "단순 레코드 조회 테이블을 글로벌 분산 스마트 라우팅 엔진으로 업그레이드"한 것이 핵심이다.

Anycast가 뭐길래 빠른가

비유하자면 이렇다. 일반 Unicast는 "서울 본사 전화번호 하나"라서 부산 사람도 서울로 전화해야 한다. Anycast는 같은 IP(전화번호)를 전 세계 119개 지점이 동시에 광고해서, 네트워크가 알아서 "가장 가까운 지점"으로 연결해준다. 부산 사람은 부산 지점이 받는다.

실제로 Anycast가 도는지는 같은 도메인의 권한 네임서버를 다른 지역에서 traceroute 떠보면 경로가 달라지는 걸로 간접 확인할 수 있다. dig로 응답 지연을 보는 게 가장 간단하다.

$ dig @8.8.8.8 example.com A +stats

;; ANSWER SECTION:
example.com.		300	IN	A	93.184.216.34

;; Query time: 12 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Tue Jun 24 14:03:21 KST 2026
;; MSG SIZE  rcvd: 56

여기서 Query time이 권한 서버까지 포함한 응답 시간이다. Anycast가 잘 깔린 DNS는 한국에서 쏴도 보통 한 자릿수~수십 ms 안에 들어온다. 캐시 미스 상태에서 권한 서버를 직접 때려봐야 진짜 레이턴시가 나온다.

Smart record / 헬스체크

원문에서 강조하는 건 latency 데이터, 헬스체크, 심지어 JavaScript로 응답을 동적 결정한다는 점이다. 쉽게 말해 "이 사용자가 어디서 왔고 어느 오리진이 살아있는지 보고 A 레코드를 즉석에서 골라준다"는 거다. Route53의 Latency-based routing + Health check 조합과 개념적으로 비슷한데, Bunny는 이걸 무료 티어에 포함시켰다는 게 차별점이다.

3. 실무 관점: 마이그레이션, 트레이드오프, 흔한 함정

마이그레이션 체크리스트

DNS 이전은 잘못하면 도메인 전체가 죽는 작업이다. 순서가 중요하다.

  1. 현재 존 레코드 전량 백업 — 이게 1순위다.
  2. 새 DNS(Bunny)에 동일 레코드 전부 입력 (Bunny는 자동 존 스캔 + BIND 파일 업로드 지원)
  3. 이전 전에 양쪽 응답이 일치하는지 검증
  4. 이전 며칠 전부터 SOA/NS의 TTL을 짧게 낮춰두기
  5. 레지스트라에서 네임서버 변경
  6. 전 세계 전파 모니터링 (보통 수십 분~48시간)

기존 존을 통째로 뽑는 가장 확실한 방법은 AXFR(존 전송)이지만, 대부분 매니지드 DNS는 보안상 AXFR을 막아둔다. 막혀있으면 콘솔에서 BIND 파일 export를 쓰거나, 주요 레코드를 직접 dig로 긁어야 한다.

# 기존 네임서버에서 존 전체를 BIND 형식으로 받아보기 (AXFR 허용 시)
$ dig @ns-old.example.com example.com AXFR > zone_backup.txt

# AXFR이 막혀있으면 이런 에러를 만난다 ↓
$ dig @ns-old.example.com example.com AXFR

; <<>> DiG 9.18.18 <<>> @ns-old.example.com example.com AXFR
;; global options: +cmd
; Transfer failed.

; Transfer failed.가 떴다면 AXFR이 거부된 거다. 당황하지 말고 콘솔 export로 우회하면 된다.

흔한 함정 1: 네임서버를 바꿨는데 안 바뀐다

가장 자주 보는 증상. 레지스트라에서 NS를 바꿨는데 한참 옛날 IP가 돌아온다. 이건 보통 리졸버 캐시거나, 존 안에 박혀있는 NS 레코드레지스트라(상위 위임)의 NS 레코드가 불일치할 때 생긴다. 두 곳을 다 맞춰야 한다.

위임이 제대로 됐는지는 상위(부모) 존에 직접 물어봐야 한다.

# .com 권한 서버에 위임 정보를 직접 물어보기
$ dig +trace example.com NS

example.com.		172800	IN	NS	ns1.bunny.net.
example.com.		172800	IN	NS	ns2.bunny.net.
;; Received 100 bytes from 192.5.6.30#53(a.gtld-servers.net) in 145 ms

여기서 a.gtld-servers.net(상위 서버)이 알려주는 NS가 Bunny 것으로 나오면 위임은 성공한 거다. 그래도 내 PC에서 옛날 값이 나온다면 십중팔구 로컬/사내 리졸버 캐시다.

흔한 함정 2: DNSSEC 켜고 SERVFAIL

DNSSEC은 켜는 순간 사고가 잘 난다. 가장 흔한 건 DS 레코드와 DNSKEY 불일치다. DNS 사업자를 옮기면서 한쪽엔 DNSSEC이 켜져있고 DS는 옛날 키를 가리키면 전체 도메인이 검증 실패로 죽는다.

$ dig example.com A +dnssec

;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 41552
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

status: SERVFAIL이 DNSSEC 체인 깨졌을 때 전형적으로 보는 화면이다. DNS 사업자 마이그레이션 시에는 옮기기 전에 기존 쪽 DNSSEC을 끄고(레지스트라의 DS도 제거), TTL 기다린 뒤 옮기고, 새 쪽에서 다시 켜는 순서가 안전하다.

참고로 Bunny는 DNSSEC을 NSEC Black Lies 방식으로 구현했다고 한다. 전통 NSEC은 존 전체 레코드를 추측(zone walking)당할 수 있는데, Black Lies는 그걸 막으면서 검증은 유지한다. 보안에 민감한 조직이면 의미 있는 디테일이다.

트레이드오프: 무료의 대가

  • SLA 보장 수준 — DNS는 다운되면 서비스 전체가 죽는다. 무료 티어에 명시적 가용성 SLA가 어떻게 걸리는지는 계약서/약관 직접 확인 필요. "무료니까 보장 안 해도 할 말 없다"는 리스크는 항상 있다.
  • 벤더 락인 — 1-Click Acceleration으로 DNS에서 CDN을 바로 켜는 구조는 편한 만큼 Bunny 생태계에 묶인다. CDN까지 같이 쓸 거면 장점, DNS만 떼서 쓸 거면 굳이일 수 있다.
  • 지원 — 장애 났을 때 응답 속도. 미션 크리티컬이면 유료 엔터프라이즈 지원이 깔린 곳이 마음 편하다.

대안은 명확하다. AWS에 다 몰빵돼 있으면 Route53이 통합 면에서 편하고, 글로벌 무료 + 안정성 실적이면 Cloudflare가 검증돼 있다. Bunny는 CDN을 Bunny로 쓰거나, 쿼리 과금 불안에서 벗어나고 싶은 경우에 매력적이다.

TTL 설계 한 줄 팁

평상시엔 A/AAAA TTL을 300~3600초로 적당히 길게(쿼리 줄여 안정·비용↓), 마이그레이션이나 페일오버 직전 며칠은 60~300초로 낮춰서 빠른 전파를 확보하는 게 정석이다. Bunny는 쿼리 과금이 없으니 TTL을 짧게 가져가는 부담이 상대적으로 적다는 게 이번 변화의 실무적 의미다.

4. 정리: 누가 언제 써야 하나

한 줄 요약: Bunny DNS 무료화는 "쿼리 폭증 = 비용 폭증" 공포를 없앤 변화이고, CDN까지 Bunny로 묶을 거면 특히 합이 좋다.

  • 쓰면 좋은 경우: Bunny CDN/Shield를 이미 쓰거나 검토 중, 트래픽 변동성이 커서 쿼리 과금이 부담, 짧은 TTL로 빠른 페일오버가 필요한 스타트업~스케일업.
  • 굳이 안 옮겨도 되는 경우: AWS에 모든 게 묶여 Route53 통합이 더 가치 있을 때, 이미 Cloudflare로 잘 돌고 있고 옮길 이유가 없을 때, 계약상 명시적 DNS SLA가 반드시 필요한 금융/엔터프라이즈.

결론적으로 "무료니까 일단 테스트 존 하나 올려서 dig로 응답 시간 재보고 판단하라"가 현실적인 답이다. 메인 도메인부터 옮기지 말고, 안 중요한 도메인으로 먼저 굴려보자.

참고 자료

※ 본문의 요금·수치는 각 사 정책 변경에 따라 달라질 수 있으니 도입 전 공식 페이지에서 반드시 재확인하세요.

728x90
728x90

1. 도입: 왜 지금 이게 화제인가

요 몇 년 사이에 "남이 짠(혹은 AI가 짠) 코드를 내 인프라에서 안전하게 실행해줘야 하는" 서비스가 부쩍 늘었다. AI 코딩 어시스턴트, 온라인 IDE, 데이터 분석 노트북 플랫폼, 취약점 스캐너, 유저 스크립트를 받는 게임 서버까지. 공통점은 하나다. 각 사용자/세션마다 격리된 실행 환경을 따로 쥐어줘야 한다는 것.

여기서 실무자가 항상 부딪히는 삼각딜레마가 있다.

  • VM(EC2 등): 격리는 확실한데 부팅에 분 단위가 걸린다. 유저가 "Run" 누르고 1분 기다리게 할 수는 없다.
  • 컨테이너: 빠르게 뜨지만 커널을 공유한다. 신뢰할 수 없는 코드를 돌리려면 seccomp, gVisor, 네임스페이스 하드닝을 직접 다 발라야 한다. 이거 제대로 하려면 보안 전담 엔지니어 한 명이 통째로 붙는다.
  • 일반 Lambda 함수: 이벤트-요청/응답 모델에 최적화돼 있어서, 세션 동안 메모리·디스크 상태를 유지해야 하는 장시간 인터랙티브 워크로드엔 맞지 않는다.

그래서 지금까지는 다들 Firecracker 위에 자체 오케스트레이션을 직접 짜서 굴렸다. 스타트업이 이걸 직접 만들면 제품 만들 시간을 인프라 빌딩에 다 갈아넣게 된다. 이번에 나온 AWS Lambda MicroVMs가 정확히 이 틈을 메우는 물건이다. Lambda 함수가 이미 매달 15조 회 이상의 호출을 굴려온 그 Firecracker를 그대로 가져다, "이미지 만들고 → 띄우고 → 상태 유지하고 → 쉬면 멈추는" 라이프사이클을 API로 노출했다.

2. 핵심: 동작 원리를 예시로

image-then-launch 모델

일반 Lambda는 "코드 zip 올리면 끝"이지만, MicroVM은 두 단계다.

  1. MicroVM Image 생성: Dockerfile + 코드 zip을 S3에 올리면, Lambda가 Dockerfile을 빌드하고 애플리케이션을 실제로 한 번 띄운 다음, 그 시점의 메모리·디스크 상태를 Firecracker 스냅샷으로 떠둔다.
  2. MicroVM 실행(launch): 그 이미지에서 인스턴스를 띄울 때, 콜드 부팅을 하는 게 아니라 미리 초기화된 스냅샷에서 resume한다. 그래서 띄우는 순간 이미 앱이 떠 있는 상태로 시작한다.

비유하자면, 일반 컨테이너가 매번 "전원 켜고 OS 부팅하고 앱 띄우기"라면, MicroVM은 "노트북 덮개 닫았다가 다시 여는" 식이다. 절전(suspend)됐다가 깨어날 때 열어둔 탭이 그대로 살아있는 그 경험을 인프라 단에 적용했다고 보면 된다.

격리 원리: 커널을 공유하지 않는다

핵심은 여기다. 컨테이너는 호스트 커널 하나를 여러 컨테이너가 나눠 쓴다. 그래서 커널 취약점 하나 터지면 컨테이너 탈출(escape) 시나리오가 현실이 된다. 반면 MicroVM은 세션마다 독립된 게스트 커널과 가상화된 하드웨어를 가진다. 하이퍼바이저(KVM) 경계로 격리되기 때문에, A 유저가 던진 악성 코드가 B 유저 환경이나 호스트로 넘어가는 경로가 원천적으로 막힌다. 이게 "VM-level isolation"의 의미다.

실제 만들어보기

원문 예제 기준으로 Flask 앱을 MicroVM으로 만드는 흐름이다. 먼저 Dockerfile은 MicroVM 전용 베이스 이미지를 쓴다는 점이 포인트다.

# Dockerfile
FROM public.ecr.aws/lambda/microvms:al2023-minimal
RUN dnf install -y python3 python3-pip && dnf clean all
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

이미지 생성 명령. 코드 zip은 S3에 올려두고 URI로 참조한다.

aws lambda-microvms create-microvm-image \
  --code-artifact uri=s3://my-bucket/path/to/artifact.zip \
  --name flask-sandbox \
  --base-image-arn arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1 \
  --build-role-arn arn:aws:iam::123456789012:role/MicroVMBuildRole

빌드 로그는 CloudWatch /aws/lambda/microvms/<image-name>로 실시간 스트리밍된다. 빌드가 끝나면 ARN과 버전이 콘솔에 뜬다. 빌드 완료 후 실행:

aws lambda-microvms run-microvm \
  --image-identifier arn:aws:lambda:us-east-1:123456789012:microvm-image:flask-sandbox \
  --execution-role-arn arn:aws:iam::123456789012:role/MicroVMExecutionRole \
  --idle-policy '{"maxIdleDurationSeconds":900,"suspendedDurationSeconds":300,"autoResumeEnabled":true}'

idle-policy는 "15분(900초) 동안 요청 없으면 자동 suspend, 그 뒤 5분(300초) suspend 상태 유지, 요청 들어오면 자동 resume"이라는 뜻이다. 별도 네트워킹 세팅 없이 Lambda가 고유 ID와 전용 엔드포인트 URL을 돌려준다. 응답은 대략 이런 형태로 보인다(필드명은 공식 문서 확인 필요):

{
    "microVmId": "mvm-0a1b2c3d4e5f67890",
    "endpointUrl": "https://mvm-0a1b2c3d4e5f67890.lambda-microvms.us-east-1.amazonaws.com",
    "state": "RUNNING"
}

트래픽을 보낼 땐 단명(short-lived) 인증 토큰을 만들어 X-aws-proxy-auth 헤더에 붙인 평범한 HTTPS 요청을 던진다.

curl https://mvm-0a1b2c3d4e5f67890.lambda-microvms.us-east-1.amazonaws.com/ \
  -H "X-aws-proxy-auth: <short-lived-token>"

# 응답
{"message":"Hello, World!"}

여기서 인상적인 건, idle 임계치를 넘겨 suspend된 뒤 다시 요청을 보내도 애플리케이션 상태가 그대로 살아서 resume된다는 점이다. 클라이언트 입장에선 멈췄던 적이 없는 것처럼 보인다. 설치한 패키지, 로딩한 모델, 작업 중이던 파일이 다 그 자리에 있다.

3. 실무 관점: 트레이드오프와 흔한 함정

스냅샷 모델이 만드는 함정 — 초기화 코드를 의심하라

가장 먼저 발 헛디딜 곳이 여기다. MicroVM은 "한 번 초기화한 상태"를 스냅샷으로 떠서 모든 인스턴스가 그걸 복제한다. 즉 이미지 빌드 시점에 단 한 번 실행된 코드의 결과물이 모든 세션에 그대로 복사된다.

이게 왜 문제냐. 초기화 단계에서 다음을 하는 코드는 위험하다.

  • 고유값 생성: 부팅 시 UUID, 랜덤 시드, 세션 키를 한 번 만들어 캐싱하는 코드 → 모든 MicroVM이 동일한 값을 갖게 된다. 난수 생성기 상태까지 복제되므로 보안적으로 치명적일 수 있다.
  • 네트워크 커넥션 선점: 초기화 때 DB 커넥션 풀이나 keep-alive 소켓을 열어두면, 스냅샷에 박제된 끊긴 커넥션을 resume 후 그대로 들고 있게 된다.
  • 초기화 시점 외부 데이터 로딩: 빌드 시점 환경에서 받아온 ephemeral 데이터가 그대로 굳는다.

원문도 이 부분을 명시한다 — "고유 콘텐츠 생성, 네트워크 연결 수립, ephemeral 데이터 로딩을 하는 앱은 서비스가 제공하는 hook과 연동해야 호환된다"고. 일반 Lambda의 SnapStart를 써본 사람이라면 익숙한 패턴이다. 네트워크 연결과 난수 시드 같은 건 스냅샷 이후, 즉 resume 시점에 다시 초기화하도록 코드를 짜야 한다. 이걸 모르고 그냥 컨테이너 코드 들고 오면 이런 식의 증상을 만난다:

# resume 후 박제된 죽은 DB 커넥션을 그대로 사용하다 터지는 전형적 에러
psycopg2.OperationalError: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.

# 혹은 redis 클라이언트의 경우
redis.exceptions.ConnectionError: Error 104 while writing to socket. Connection reset by peer.

해결은 lazy connection(첫 요청 때 연결)이나 resume hook에서 풀 재생성. 일반 Lambda의 afterRestore 훅과 비슷한 메커니즘이 제공될 것으로 보이나, 정확한 API는 공식 Developer Guide 확인이 필요하다.

베이스 이미지 잘못 쓰면 빌드부터 막힌다

MicroVM은 일반 Lambda 베이스 이미지가 아니라 public.ecr.aws/lambda/microvms:... 계열을 써야 한다. 기존 Lambda 컨테이너 이미지(public.ecr.aws/lambda/python 등)를 그대로 가져오면 빌드/실행 단계에서 막힌다. 빌드 IAM 역할 권한이 부족하면 보통 이런 식으로 떨어진다:

An error occurred (AccessDeniedException) when calling the CreateMicrovmImage operation:
User: arn:aws:iam::123456789012:role/MicroVMBuildRole is not authorized to perform:
s3:GetObject on resource: arn:aws:s3:::my-bucket/path/to/artifact.zip

build-role과 execution-role을 헷갈려서 권한을 한쪽에만 몰아주는 실수가 잦다. 빌드 역할은 S3 아티팩트 읽기 + CloudWatch 로그 쓰기, 실행 역할은 런타임에 필요한 권한으로 나눠 생각하는 게 깔끔하다.

리소스·리전·아키텍처 제약

  • 아키텍처: ARM64만 지원한다. x86 전용 바이너리(특정 ML 라이브러리 등) 쓰던 팀은 빌드 단계에서 멀티아키 대응이 필요하다. 이거 의외로 마이그레이션 발목 자주 잡는다.
  • 스펙 상한: MicroVM당 최대 16 vCPU / 32GB 메모리 / 32GB 디스크. 무거운 LLM을 풀로 메모리에 올리기엔 32GB가 빠듯할 수 있다.
  • 총 런타임: 최대 8시간. 이걸 넘기는 장시간 세션(종일 켜두는 워크스테이션 같은)에는 부적합. 세션 종료/재시작 설계를 미리 해둬야 한다.
  • 리전: 출시 시점 기준 버지니아 북부, 오하이오, 오레곤, 아일랜드, 도쿄. 서울 리전은 아직 없다. 한국 서비스라면 도쿄로 붙이거나 정식 출시(GA)/서울 확장을 기다려야 한다. 지연 시간 민감한 인터랙티브 워크로드면 이게 실질적 결정 요인이 된다.

비용 관점

핵심 비용 절감 포인트는 suspend다. idle 상태로 멈춰두면 running 비용이 낮은 idle 비용으로 떨어지고 상태는 보존된다. 즉 "유저가 잠깐 자리 비운 동안 풀로 과금"되는 걸 피할 수 있다. 다만 suspend 상태에서도 스냅샷 저장 비용은 발생할 것으로 보이며, 구체 단가는 Lambda 요금 페이지 확인이 필요하다. 인터랙티브 세션이 많고 idle 비율이 높은 서비스일수록 이득이 크다.

그래서 언제 일반 Lambda를 쓰고 언제 MicroVM을 쓰나

  • 이벤트 기반, 짧은 요청/응답, 무상태 → 그냥 일반 Lambda 함수. MicroVM은 오버킬이고 더 비싸다.
  • 신뢰 못 할 코드 실행 + 세션 상태 유지 + 빠른 시작MicroVM.
  • 장시간(8시간 초과) 상시 가동, 풀 커스텀 OS 제어, GPU → 여전히 EC2 혹은 ECS/EKS on EC2.
  • 빠른 시작이 필요하지만 신뢰된 자사 코드만 돌림컨테이너(ECS/Fargate)로 충분한 경우가 많다.

원문 표현대로 둘은 경쟁이 아니라 보완 관계다. 이벤트 백본은 Lambda 함수로 두고, 신뢰 못 할 코드를 격리 실행해야 하는 구간만 MicroVM을 호출하는 조합이 정석으로 보인다.

4. 정리

한 줄 요약: MicroVM은 "VM 수준 격리 + 컨테이너 수준 시작 속도 + 세션 상태 보존"을 직접 빌드하지 않고 API로 얻는 서버리스 프리미티브다.

누가 써야 하나 — 멀티테넌트 SaaS에서 유저/AI가 생성한 코드를 안전하게 돌려야 하는 팀. AI 코딩 어시스턴트, 온라인 IDE, 데이터 분석 플랫폼, 취약점 스캐너가 1순위 후보다. 지금까지 Firecracker 위에 자체 샌드박스 오케스트레이션을 직접 짜서 굴리던 팀이라면, 그 유지보수 부담을 통째로 덜 수 있다는 게 가장 큰 가치다.

반대로, 무상태 이벤트 처리만 하거나 8시간 넘는 상시 워크로드, 혹은 서울 리전 저지연이 필수인 서비스라면 아직은 보류하거나 다른 선택지를 봐야 한다. 스냅샷 기반 초기화의 함정(난수·커넥션 박제)은 도입 전에 반드시 코드 리뷰로 걸러야 할 부분이다.

참고 자료

※ 본문의 응답 JSON 필드명, idle-policy 세부 스펙, resume hook API 등은 출시 직후 시점 기준 추정이 포함돼 있습니다. 실제 도입 전 Lambda MicroVMs Developer Guide의 최신 내용을 반드시 확인하세요.

728x90
728x90

도입: 트래픽 스파이크 때마다 한 박자 늦던 그 답답함

ECS로 서비스 운영해본 사람이라면 한 번쯤 겪어봤을 거다. 트래픽이 갑자기 튀는데 태스크는 한참 뒤에야 늘어나고, 그 사이에 응답 지연이나 5xx가 줄줄이 터지는 상황. 모니터링 대시보드 보면서 "왜 아직도 안 늘어나?" 하다가 결국 평소에 태스크를 넉넉하게 띄워놓는 식으로 땜빵하게 된다. 비용은 비용대로 나가고.

이 문제의 근본 원인 중 하나가 CloudWatch 메트릭의 해상도였다. 기존 ECS 서비스 오토스케일링은 기본적으로 1분(60초) 단위 메트릭에 의존했다. CPU가 임계치를 넘어도 메트릭이 집계되고 알람이 평가되고 스케일링이 트리거되기까지 분 단위 지연이 깔려 있었다는 얘기다.

이번에 AWS가 ECS 서비스 오토스케일링에 20초 해상도 고해상도 메트릭을 지원하기 시작했다. 단순한 숫자 변경이 아니라, AWS 자체 벤치마크 기준으로 스케일 아웃 트리거 시간이 363초 → 86초(76% 단축, 4.2배), 새 태스크 프로비저닝까지 포함한 전체 시간이 386초 → 109초(72% 단축, 3.5배)로 줄었다고 한다. 트래픽 대응과 비용 최적화에 직결되는 변화라 안 짚고 넘어갈 수가 없다.

핵심: 1분에서 20초로, 무엇이 달라지나

왜 1분 메트릭이 느렸나

타깃 트래킹 스케일링이 동작하는 흐름을 쪼개보면 이렇다.

  1. ECS/CloudWatch가 CPU·메모리 같은 지표를 수집하고 집계한다.
  2. CloudWatch 알람이 일정 주기로 메트릭을 평가한다.
  3. 임계치를 넘으면 Application Auto Scaling이 스케일링 액션을 발동한다.
  4. ECS가 새 태스크를 띄우고 ALB에 등록하고 헬스체크를 통과한다.

1분 메트릭에서는 1~2단계에서만 이미 수십 초~1분 이상이 깔린다. 메트릭이 1분에 한 번 찍히니, 트래픽이 튄 직후의 데이터 포인트가 다음 분이 되어야 반영된다. 거기에 알람 평가 주기까지 더해지면 "지표상 부하가 올랐다"는 사실을 인지하는 데만 수 분이 걸린다.

고해상도 메트릭의 동작

고해상도 메트릭은 이 집계·평가 주기를 20초 단위로 당긴다. 비유하자면, 기존이 1분마다 한 번 창밖을 내다보고 비가 오는지 판단하던 거라면, 고해상도는 20초마다 내다본다. 비 오기 시작하면 더 빨리 우산을 펼 수 있는 거다.

이번 기능에서 새로 추가된 미리 정의된 메트릭은 두 개다.

  • ECSServiceAverageCPUUtilizationHighResolution
  • ECSServiceAverageMemoryUtilizationHighResolution

이름에서 보이듯 평균 CPU 사용률과 평균 메모리 사용률 기반이다. 타깃 트래킹 정책에서 이 메트릭을 고르면 ECS 서비스가 20초 간격으로 스케일링 결정을 평가하게 된다. AWS Fargate, ECS Managed Instances, EC2 등 모든 컴퓨트 옵션에서 동작한다.

설정은 두 단계

핵심은 두 가지다. (1) 서비스에 20초 해상도 메트릭을 켠다. (2) 그 메트릭을 쓰는 타깃 트래킹 정책을 건다. 콘솔이라면 서비스 생성/수정 시 Monitoring 섹션에서 20초 해상도 메트릭을 추가하고, Service auto scaling 섹션에서 Target Tracking을 고른 뒤 위의 HighResolution 메트릭을 선택하면 끝이다.

CLI로 한다면 대략 이런 흐름이다. 먼저 서비스에 고해상도 메트릭을 활성화한다.

aws ecs update-service \
  --cluster my-cluster \
  --service my-web-service \
  --service-connect-configuration ... \
  --enable-high-resolution-metrics 2>&1 || true

# 실제 플래그 명칭/위치는 최신 CLI 버전에 따라 다를 수 있으니
# `aws ecs update-service help` 로 확인하는 걸 권장한다.

※ 위 플래그명은 환경/버전에 따라 다를 수 있어 공식 문서 확인이 필요하다. 메트릭이 켜진 뒤 배포가 완료되면 고해상도 메트릭이 생성되기 시작한다.

그다음 Application Auto Scaling으로 타깃 트래킹 정책을 건다. CPU 60%를 타깃으로 잡는 예시다.

# 1) 스케일 대상 등록
aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/my-cluster/my-web-service \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 20

# 2) 고해상도 메트릭 기반 타깃 트래킹 정책 생성
cat > policy.json <<'EOF'
{
  "TargetValue": 60.0,
  "PredefinedMetricSpecification": {
    "PredefinedMetricType": "ECSServiceAverageCPUUtilizationHighResolution"
  },
  "ScaleInCooldown": 60,
  "ScaleOutCooldown": 60
}
EOF

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/my-cluster/my-web-service \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name cpu-highres-tt \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration file://policy.json

정상적으로 들어가면 정책 ARN과 생성된 CloudWatch 알람 정보가 출력된다.

{
    "PolicyARN": "arn:aws:autoscaling:ap-northeast-2:111122223333:scalingPolicy:...:resource/ecs/service/my-cluster/my-web-service:policyName/cpu-highres-tt",
    "Alarms": [
        {
            "AlarmName": "TargetTracking-service/my-cluster/my-web-service-AlarmHigh-xxxxxxxx",
            "AlarmARN": "arn:aws:cloudwatch:ap-northeast-2:111122223333:alarm:..."
        },
        {
            "AlarmName": "TargetTracking-service/my-cluster/my-web-service-AlarmLow-xxxxxxxx",
            "AlarmARN": "arn:aws:cloudwatch:ap-northeast-2:111122223333:alarm:..."
        }
    ]
}

예전에는 이 정도의 공격적인 스케일링을 하려면 스텝 스케일링 정책을 손으로 정교하게 짜야 했는데, 이제는 타깃 트래킹 + 고해상도 메트릭 조합으로 설정 한 번에 비슷한 반응성을 얻을 수 있다는 게 AWS의 설명이다. 그동안 커스텀 엔지니어링으로 땜빵하던 부분이 설정 하나로 대체되는 셈이다.

실무 관점: 트레이드오프와 흔한 함정

비용 — 공짜가 아니다

오해하기 쉬운 부분인데, "빠른 오토스케일링" 기능 자체는 추가 비용이 없다. 다만 고해상도 메트릭이 새로운 과금 차원을 만든다. 표준 해상도(60초) 메트릭은 무료지만, 20초 해상도 메트릭은 CloudWatch 비용이 추가로 발생한다. 정확한 단가는 원문에 명시되어 있지 않으니 CloudWatch 요금 페이지를 직접 확인해야 한다.

그래서 트레이드오프 판단이 중요하다. 고해상도 메트릭 비용을 더 내더라도, 평소 태스크를 넉넉하게 띄워두던 "선제적 패딩"을 줄일 수 있다면 전체 컴퓨트 비용은 오히려 내려갈 수 있다. 스케일 아웃이 충분히 빨라지니까 미리 capacity를 깔아둘 필요가 줄어드는 거다. 반대로 트래픽이 늘 잔잔하고 스파이크가 거의 없는 서비스라면 굳이 고해상도까지 켤 이유가 없다.

워크로드별 권장 (개인 판단 기준)

  • 스파이크가 잦고 급격한 서비스(이벤트성 트래픽, 외부 캠페인 연동 API 등): 고해상도 켤 만하다. 스케일 아웃 지연이 곧 장애로 이어지는 케이스.
  • 완만하게 증감하는 서비스: 60초로도 충분한 경우가 많다. 비용 대비 이득이 작다.
  • 큐 기반 워커(SQS depth 등 커스텀 메트릭): 이번에 추가된 건 CPU/메모리 고해상도 프리디파인드 메트릭이라, 큐 깊이 같은 커스텀 메트릭은 별도로 고해상도로 publish하는 설계가 필요하다. 적용 가능 여부는 문서 확인 필요.

흔한 함정

함정 1 — 메트릭을 안 켜고 정책부터 만든다. 서비스에 고해상도 메트릭을 활성화하고 배포가 완료되기 전에는 HighResolution 메트릭에 데이터 포인트가 안 찍힌다. 이 상태로 타깃 트래킹을 걸면 알람이 데이터 부족 상태로 빠지고 스케일링이 동작하지 않는다. 콘솔이나 알람에서 이런 메시지를 보게 된다.

State: INSUFFICIENT_DATA
StateReason: Insufficient Data: 1 datapoint were unknown.

순서가 핵심이다. 메트릭 활성화 → 배포 완료 → 메트릭 생성 확인 → 정책 연결 순으로 가야 한다.

함정 2 — 쿨다운을 그대로 둔다. 메트릭 평가는 20초로 빨라졌는데 ScaleOutCooldown을 기존처럼 300초로 길게 잡아두면, 정작 추가 스케일 아웃이 쿨다운에 막혀서 고해상도의 이점이 반감된다. 위 예시처럼 쿨다운도 짧게(예: 60초) 재검토하는 게 좋다. 단, 너무 짧으면 출렁임(flapping)이 생길 수 있으니 실측하며 조정해야 한다.

함정 3 — 다운스트림 한계를 무시한다. ECS 태스크는 빨리 늘어나는데 DB 커넥션 풀이나 외부 API rate limit이 그걸 못 받쳐주면, 빠른 스케일 아웃이 오히려 다운스트림을 때려서 장애를 키운다. 스케일링이 빨라질수록 "내 서비스의 진짜 병목이 어디인가"를 다시 봐야 한다.

함정 4 — 프로비저닝 한계를 메트릭으로 해결하려 한다. 벤치마크에서 트리거는 86초로 줄었지만 전체 시간은 109초였다. 즉 태스크 이미지 풀, 컨테이너 기동, ALB 등록·헬스체크에 걸리는 시간은 여전히 남는다. 이미지 슬림화, 헬스체크 주기 튜닝 같은 기본기가 같이 받쳐줘야 체감 효과가 산다.

모니터링 대시보드 팁

고해상도 전환 후에는 대시보드 위젯의 period도 같이 손봐야 한다. 위젯을 60초 period로 두면 모처럼 켠 20초 데이터의 디테일이 뭉개진다. CPU/메모리 위젯은 20초 또는 그에 맞는 짧은 period로 보고, 옆에 DesiredCount와 RunningCount, ALB의 TargetResponseTime/5xx를 나란히 두면 "지표가 오름 → 태스크가 늚 → 지연이 잡힘"이라는 인과를 한눈에 검증할 수 있다.

정리

한 줄 요약: ECS 서비스 오토스케일링이 20초 해상도 메트릭을 지원하면서 스케일 아웃 반응이 약 4배 빨라졌고, 그동안 스텝 스케일링으로 땜빵하던 공격적 스케일링을 타깃 트래킹 설정 하나로 대체할 수 있게 됐다.

누가 언제: 트래픽 스파이크가 잦아서 평소 태스크를 넉넉히 깔아두던 서비스라면 지금 검토할 가치가 충분하다. 고해상도 메트릭 비용은 추가되지만, 선제적 패딩을 줄여 전체 컴퓨트 비용을 낮추는 방향으로 충분히 상쇄할 수 있다. 반대로 트래픽이 완만한 서비스나, 병목이 ECS 태스크가 아니라 DB·외부 API 쪽인 경우엔 효과가 제한적이니 무작정 켜지 말고 트레이드오프를 따져보자. 적용할 땐 메트릭 활성화 순서와 쿨다운 재조정을 꼭 챙겨야 한다.

참고 자료

728x90

+ Recent posts