728x90

얼마 전 Hacker News에 올라온 글 하나가 눈에 걸렸다. GPU Mode가 Core Automation과 같이 연 "오토리서치" 주제 대회에서, 저자(sankalp)가 Codex를 돌려 baseline 대비 232배 빠른 QR 분해 커널을 만들고 183명 중 12위를 했다는 이야기다. 14일 동안 1500번 넘게 제출했다고 한다.

커널 최적화 자체는 내 밥벌이가 아니다. 그런데 이 글을 읽다 보니 결국 우리가 매일 하는 일이더라. 피드백 루프를 얼마나 촘촘하고 싸게 만드느냐. 그 관점에서 정리해본다.

1. 왜 지금 이 글이 화제인가

요즘 "에이전트에게 코드 짜게 시켰다" 류 글은 널렸다. 이 글이 다른 지점은 목적함수가 숫자 하나로 딱 떨어진다는 점이다.

  • 정답 판정: 체커가 torch.linalg.householder_product(H, tau)로 Q를 복원하고 A ≈ QR, QᵀQ ≈ I를 검증한다. 통과 아니면 탈락, 애매함이 없다.
  • 점수: 여러 shape에 대한 런타임의 기하평균. 512×512부터 1024, 2048, 4096까지.
  • 제출 채널: popcorn CLI. 에이전트가 직접 테스트·벤치·리더보드 제출까지 한다.
  • 피드백: 전체 기하평균뿐 아니라 shape별 타이밍까지 돌려준다.

이게 왜 중요하냐면, 에이전트가 hill-climbing을 하려면 "지금 것이 이전 것보다 나은가"를 사람 개입 없이 판정할 수 있어야 한다. 원문 표현대로 "Agents yearn for tight feedback loops"다. 우리 판으로 옮기면 이렇다.

  • ❌ "이 Terraform 코드 좀 더 깔끔하게 해줘" → 판정 불가, 루프 못 돈다
  • ✅ "이 Helm 차트 렌더링 결과가 conftest 정책 22개를 다 통과하고, kustomize build 시간이 짧을수록 좋다" → 루프 가능

즉 이 글의 진짜 주제는 GPU가 아니라 "검증 가능한 목표 + 무제한에 가까운 시행횟수"라는 환경을 어떻게 세팅했나다. 저자 본인도 "loop engineering이라 불러도 상관없다"고 인정한다.

2. 핵심 원리: 순차 의존을 깨고, 루프를 돌린다

문제 쪽 — 왜 이 커널이 느렸나

Householder QR은 열(column) 하나씩 처리한다. j번째 reflector가 만든 결과 위에서 j+1번째 reflector를 만들어야 한다. 즉 순차 의존 체인이다. 원문의 표현을 빌리면, 이 직렬 matrix-vector 연산은 SM의 느린 벡터 레인에서 돌고 텐서 코어는 놀고 있다.

DevOps로 치면 이런 배포 파이프라인이다.

service-a 배포 → 헬스체크 → service-b 배포 → 헬스체크 → ... (n번 반복)

각 단계가 앞 단계 결과에 의존하니 병렬화가 안 된다. 노드는 100대 있는데 한 번에 한 대만 일한다. 고전적인 해법은 blocked 알고리즘이다. b개 열(예: 32, 64)을 하나의 panel로 묶어 직렬 작업은 그 좁은 패널 안에서만 끝내고, 나머지 거대한 trailing 블록은 WY 형태의 행렬곱 한 방(GEMM)으로 갱신한다. "직렬 구간을 최대한 좁게 만들고, 나머지는 통째로 병렬화한다" — 배포 파이프라인에서 canary 1대만 순차로 검증하고 나머지 99대는 한꺼번에 밀어버리는 것과 정확히 같은 발상이다.

루프 쪽 — 사람이 한 일

주목할 부분은 저자가 먼저 공부했다는 점이다. Claude랑 왔다 갔다 하고 유튜브를 보면서 Gram-Schmidt와 Householder의 차이, blocked Householder + trailing WY-update가 메인 아키텍처가 되어야 한다는 것까지 잡고 들어갔다. 원문의 이 문장이 핵심이다.

더 잘 알수록 LLM에게 더 잘 프롬프트할 수 있다. 왜냐하면 unknown unknowns를 known unknowns로 바꾸기 때문이다.

이건 인프라에서도 똑같다. "쿠버네티스 파드가 자꾸 죽어요"라고 던지면 에이전트는 온갖 삽질을 한다. "OOMKilled인데 JVM 힙은 여유가 있고 native memory 쪽 같다, RSS 추이 좀 보자"라고 하면 탐색 공간이 100분의 1로 줄어든다. 도메인 지식은 에이전트의 탐색 공간을 잘라내는 가지치기 도구다.

동시에 저자는 솔직하다. "도메인 지식 없이도 이 대회는 가능했다. Top 10은 못 가겠지만 harness/에이전트 루프만으로도 baseline 대비 그럴듯한 speedup은 나온다." 딱 이 정도가 현재 에이전트의 실력 위치라고 보면 된다.

루프를 흉내내보기

거창할 것 없다. "변경 → 검증 → 점수 비교 → 좋으면 채택"이 전부다. 인프라 쪽에서 바로 돌려볼 수 있는 최소 형태를 만들어보자.

#!/usr/bin/env bash
# bench-loop.sh — 후보 매니페스트를 검증하고 렌더 시간을 점수로 기록
set -euo pipefail
CAND="${1:?usage: ./bench-loop.sh candidates/v3}"

# 1) 정답성: 정책 위반 있으면 즉시 실패 (점수 계산 안 함)
if ! kustomize build "$CAND" | conftest test -p policy/ - >/tmp/gate.log 2>&1; then
  echo "REJECT $CAND :: $(tail -n1 /tmp/gate.log)"; exit 1
fi

# 2) 성능: 5회 반복 후 최소값을 점수로
best=99999
for i in $(seq 5); do
  s=$( { /usr/bin/time -f "%e" kustomize build "$CAND" >/dev/null; } 2>&1 )
  best=$(echo "$s $best" | awk '{print ($1<$2)?$1:$2}')
done
echo "ACCEPT $CAND score=${best}s" | tee -a results.tsv
$ ./bench-loop.sh candidates/v2
REJECT candidates/v2 :: FAIL - Deployment/api must set resources.limits.memory

$ ./bench-loop.sh candidates/v3
ACCEPT candidates/v3 score=0.84s

포인트는 두 가지다. ① 정답성 게이트를 통과 못 하면 점수 자체를 안 낸다(빠른 오답에 낚이지 않게). ② 점수를 results.tsv에 누적해서 에이전트가 과거 이력을 읽을 수 있게 한다. 원문의 체커가 shape별 타이밍을 돌려준 것과 같은 역할이다.

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

비용은 진짜 나간다

원문에서 가장 실무적인 대목은 이거다. 주최 측이 사실상 무제한 제출을 허용했는데, 다들 미친 듯이 던지는 바람에 큐가 밀려서 모두의 실행이 타임아웃났고, 어느 시점엔 워크스페이스의 Modal 크레딧이 바닥났다. 그래서 "제출 간격을 띄우라"는 조건이 붙었다.

이걸 사내 CI로 옮기면 그림이 선명하다. 에이전트 루프를 GitHub Actions나 Jenkins에 붙이는 순간, 러너 슬롯을 독차지한다. 실제로 겪는 순서는 대충 이렇다.

  1. 에이전트가 잘 돈다 → 신나서 병렬도를 올린다
  2. 다른 팀 배포 파이프라인이 큐에서 대기하기 시작한다
  3. 슬랙에 "CI 왜 이렇게 느려요?" 가 올라온다
  4. 비용 대시보드에서 이번 달 청구서를 본다

대책은 평범하지만 확실하다. 전용 러너 풀 분리 + concurrency 제한 + 세션당 하드 예산 상한. 특히 예산 상한은 "몇 달러"가 아니라 "몇 회 실행"으로 거는 게 관리하기 쉽다.

# .github/workflows/agent-loop.yml (발췌)
concurrency:
  group: agent-loop-${{ github.ref }}
  cancel-in-progress: true   # 옛 루프가 러너를 붙잡고 있지 않게
jobs:
  optimize:
    runs-on: [self-hosted, agent-pool]   # 배포용 러너와 물리적으로 분리
    timeout-minutes: 45                  # 무한 루프 방어선
    steps:
      - uses: actions/checkout@v4
      - run: ./bench-loop.sh candidates/latest

흔한 함정 1: 정답성 게이트가 헐거우면 에이전트가 그리로 샌다

루프의 목적함수가 "빠를수록 좋다"인데 정답 판정이 느슨하면, 에이전트는 계산을 건너뛰는 방향으로 최적화한다. 이 대회는 체커가 QᵀQ ≈ I와 A ≈ QR을 모두 확인했기 때문에 그 도망길이 막혀 있었다. 우리 파이프라인에도 같은 장치가 필요하다. "테스트 통과"만 걸어두면 테스트를 skip 처리하는 커밋이 올라온다. 진짜다.

수치 계산 쪽이라면 이런 에러가 게이트 역할을 한다. 저정밀도(FP16/FP8 등)를 내부적으로 쓰다가 최종 검증에서 터지는 전형적인 패턴이다.

AssertionError: Tensor-likes are not close!

Mismatched elements: 1837 / 262144 (0.7%)
Greatest absolute difference: 0.0134 at index (0, 3, 511) (up to 1e-05 allowed)
Greatest relative difference: 0.0091 at index (0, 3, 511) (up to 1.3e-06 allowed)

대회 규칙상 내부 연산에 저비트를 쓰는 건 허용됐지만 반환되는 factor는 FP32 기준 QR 검사를 통과해야 했다. "빠르지만 틀린 답"을 걸러내는 선이 정확히 여기다. 인프라 쪽 대응물은 "이 최적화가 정책/보안/SLO 중 하나라도 깨면 점수 0"이다.

흔한 함정 2: 로컬 최대값에 갇힌다

원문에도 나오는 부분이다. 같은 방향으로만 계속 개선하면 어느 순간 0.5%씩 줄어들다가 멈춘다. 저자는 아이디어 다양성(idea diversity)을 일부러 주입해서 빠져나왔다고 적었다. 실무 번역: 같은 프롬프트, 같은 시작점으로 계속 돌리지 말고 구조가 다른 후보를 병렬로 굴려라. 브랜치를 3~4개 파서 서로 다른 아키텍처 가정(예: 블록 크기 다르게, 아예 다른 라이브러리)을 각각 밀어보는 식이다. 이건 하이퍼파라미터 서치의 random restart와 발상이 같다.

흔한 함정 3: 벤치 노이즈를 점수로 착각한다

공유 러너에서 시간을 재면 옆 잡 때문에 20~30% 흔들리는 건 예사다. 그러면 에이전트는 "노이즈로 우연히 빨랐던 후보"를 채택하고 그 위에 계속 쌓는다. 며칠 뒤 돌아보면 baseline보다 느린 코드가 최적해로 등극해 있다. 방어책:

  • 여러 번 측정 후 최소값 또는 중앙값 사용(위 스크립트가 최소값을 쓴 이유)
  • 개선 폭이 측정 편차보다 작으면 채택하지 않기
  • 주기적으로 baseline을 재측정해서 러너 자체가 느려졌는지 확인

언제 안 쓰는 게 맞나 (대안)

이 방식이 안 먹히는 영역이 분명히 있다.

  • 정답 판정이 사람 판단인 일: 아키텍처 설계, 장애 원인 보고서, 온콜 정책. 루프가 돌 수 없다.
  • 1회 실행 비용이 큰 일: 스테이징 전체 재구축에 40분 걸리면 1500회는 불가능하다. 이 경우엔 먼저 싼 프록시 지표(정적 검증, 드라이런, 축소 데이터셋)를 만드는 게 선행 과제다.
  • 롤백이 어려운 일: 프로덕션 DB 마이그레이션에 루프를 붙이지 마라. 당연한 말인데 실제로 시도하는 사람이 있다.

반대로 잘 맞는 건 명확하다. 컨테이너 이미지 크기 줄이기, 빌드 시간 단축, 쿼리 튜닝, IaC 정책 통과율 — 전부 숫자 하나로 줄어들고 롤백이 공짜인 작업들이다.

4. 정리

한 줄 요약: "에이전트가 잘하는 게 아니라, 검증 가능한 목적함수와 값싼 반복이 있는 문제가 잘 풀리는 것이다."

이 글에서 챙길 것 세 가지.

  1. 루프의 품질은 체커의 품질이다. 232배는 Codex의 성능이 아니라, 정답성과 속도를 자동 판정해주는 체커가 1500번 돌아간 결과다. 우리 일에서 먼저 만들 것도 에이전트가 아니라 게이트다.
  2. 도메인 지식은 여전히 순위를 가른다. 저자는 blocked Householder + WY-update라는 방향을 미리 잡고 들어갔다. 지식 없이도 baseline은 넘지만 상위권은 못 간다는 게 저자 본인의 진단이다.
  3. 공용 자원부터 격리하라. 크레딧이 바닥나고 큐가 밀려 전원이 타임아웃난 건 대회 얘기지만, 사내 CI에서 똑같이 재현된다. 전용 러너, concurrency 제한, 실행 횟수 상한은 시작 전에 걸어두는 것이다.

지금 당장 해볼 만한 건, 팀에서 제일 자주 손대는 반복 작업 하나를 골라 "합격/불합격 + 점수 한 줄"을 뱉는 스크립트를 만드는 일이다. 에이전트를 붙이는 건 그다음이다. 그 스크립트가 없으면 에이전트는 그냥 비싼 난수 생성기다.

참고 자료

※ 커널 구현 세부(블록 크기, 정밀도 조합 등)는 원문과 대회 리더보드를 직접 확인하는 걸 권한다. 이 글은 인프라/DevOps 관점에서 "루프 설계" 부분만 뽑아 정리한 것이다.

728x90
728x90

사내 위키에 "릴리즈 노트 모아보는 대시보드 좀 만들어주세요"라는 요청이 들어왔다. Kubernetes, Terraform, 각종 CVE 공지... 제품마다 알림 채널이 제각각이라 결국 내가 꺼내든 건 20년 넘은 프로토콜, RSS였다. 그런데 막상 피드 URL을 모으다 보니 절반쯤은 죽어 있거나, 찾기가 이상하리만치 어려웠다.

왜 이렇게 됐는지 잘 정리한 글이 Open RSS의 "How Google helped destroy adoption of RSS feeds"다. Hacker News에서 주기적으로 다시 올라오는 글인데, 단순한 구글 욕이 아니라 "플랫폼이 개방형 프로토콜을 다루는 패턴"에 대한 케이스 스터디로 읽으면 실무자에게도 쓸모가 많다.

도입: 왜 피드 URL 찾기가 이렇게 어려워졌나

원문이 정리한 구글의 RSS 관련 행보를 시간순으로 요약하면 이렇다.

  • Chromium 초기 버전: 주소창에 RSS 버튼이 기본 내장. 피드가 있는 페이지면 아이콘이 떴다. 어느 날 예고도 이유도 없이 사라짐.
  • 2007년 FeedBurner 인수: 원래 피드를 구글이 소유한 사설 피드로 대체하고 광고·추적을 붙이는 서비스. 2012년 10월 API 종료, 2022년 7월에는 이메일 구독 등 대부분의 기능을 걷어냄. 구독 메일에 박혀 있던 피드 URL이 그대로 깨졌다.
  • 2013년 Google Reader 종료: "충성 사용자층은 있지만 사용량이 감소했다"는 것이 공식 사유. 당시 구글 엔지니어는 "프로젝트에 있는 내내 여러 사람이 이걸 죽이려 했다"고 말했다고 원문은 전한다.
  • Google Alerts: 2008년 10월 RSS 수신 추가 → 2013년 7월 제거 → 반발 후 복원. 다만 그땐 이미 많은 사용자가 떠난 뒤였다.
  • Chrome RSS 확장: 제거했다가 일주일 만에 "실수였다"며 복원.
  • Google News: RSS 지원을 deprecate 후 2017년 12월 완전 종료. 구글 자체 링크는 계속 잘 동작했다.
  • 2021년 5월: Chrome에 RSS 지원을 다시 넣겠다고 발표. 이후 공식 출시 소식은 없는 상태로 보인다.

원문은 이 패턴을 Embrace, Extend, Extinguish로 부른다. 개방형 프로토콜 위에 제품을 올려 신뢰와 점유율을 얻고, 락인이 끝나면 프로토콜 지원을 걷어내는 방식이다. 기술적으로 RSS가 죽은 게 아니다. 원문 첫 문장도 "RSS 피드는 살아 있고 지금도 많이 쓰인다"로 시작한다. 죽은 건 발견 가능성(discoverability)사용자 신뢰다.

핵심: RSS는 죽지 않았다, 눈에 안 보일 뿐

RSS의 동작 원리는 허무할 만큼 단순하다. 정적 XML 파일 하나를 HTTP로 GET 해오는 게 전부다. 서버 푸시도, 인증 협상도, SDK도 없다. 웹 페이지는 <head>에 다음 한 줄로 피드 위치를 알려준다.

<link rel="alternate" type="application/rss+xml"
      title="RSS" href="https://example.com/feed.xml">

브라우저의 RSS 버튼이 하던 일이 딱 이 태그를 파싱해서 아이콘을 띄우는 것이었다. 그 버튼이 사라지자, 피드는 존재하는데 일반 사용자 눈에는 안 보이는 상태가 됐다. 마치 서버에 서비스는 떠 있는데 로드밸런서에서 타겟 그룹을 빼버린 것과 같다. 헬스체크는 통과하는데 트래픽이 0인 상황.

직접 확인해보자. 임의의 블로그에서 피드 링크를 뽑아내는 한 줄짜리 명령어다.

$ curl -sL https://openrss.org/blog | \
    grep -oE '<link[^>]*application/(rss|atom)\+xml[^>]*>'

<link rel="alternate" type="application/rss+xml" title="Open RSS" href="/blog/feed">

피드를 찾았으면 실제로 긁어본다. XML 파싱은 xmllint(libxml2-utils 패키지)로 충분하다.

$ curl -sL https://news.ycombinator.com/rss | \
    xmllint --xpath '//item/title/text()' - | head -3

Show HN: I built a thing
Ask HN: How do you monitor your homelab?
The unreasonable effectiveness of plain text

여기서 중요한 포인트. 이 두 명령어는 API 키도, OAuth 토큰도, rate limit 협의도 필요 없다. 요즘 어떤 SaaS든 알림을 받으려면 앱 등록하고 스코프 정하고 토큰 로테이션 정책까지 세워야 하는 걸 생각하면, RSS의 운영 비용은 거의 0에 수렴한다. 인프라 엔지니어가 RSS를 아직 좋아하는 이유가 바로 이거다.

실무 관점: 어디에 쓰고, 어디서 터지는가

실제로 쓰는 자리

내가 굴려본 용도는 대략 이렇다.

  • 업스트림 릴리즈 추적: GitHub 릴리즈는 https://github.com/{org}/{repo}/releases.atom로 피드가 나온다. 태그는 /tags.atom. 웹훅 설정 없이 조회만으로 끝난다.
  • 보안 공지 수집: 벤더 시큐리티 어드바이저리 페이지 상당수가 여전히 RSS/Atom을 제공한다. 슬랙 채널로 흘려보내면 담당자 눈에 들어온다.
  • 상태 페이지: Statuspage 기반 서비스는 대부분 /history.rss를 제공한다. 클라우드 장애를 이메일보다 먼저 잡을 때가 있다.

파이프라인도 단순하다. cron이나 CronJob으로 주기 실행 → 마지막 처리 GUID 저장 → 신규 항목만 Webhook 전송. Python 기준 핵심만 추리면 20줄 안에 끝난다.

import feedparser, json, pathlib, requests

STATE = pathlib.Path("/var/lib/feedbot/seen.json")
seen = json.loads(STATE.read_text()) if STATE.exists() else {}

url = "https://github.com/kubernetes/kubernetes/releases.atom"
d = feedparser.parse(url, etag=seen.get("etag"))

if d.status == 304:          # 변경 없음 → 조용히 종료
    raise SystemExit(0)

known = set(seen.get("ids", []))
for e in reversed(d.entries):
    if e.id in known:
        continue
    requests.post(WEBHOOK, json={"text": f"{e.title}\n{e.link}"}, timeout=5)
    known.add(e.id)

STATE.write_text(json.dumps({"etag": d.get("etag"), "ids": list(known)[-200:]}))

etag를 넘겨서 304를 받으면 바로 빠지는 부분이 핵심이다. 이걸 빼먹고 5분마다 전체 피드를 받아오면 상대 서버 입장에서는 그냥 봇 트래픽이다. 실제로 차단당한다.

흔한 함정

1) 파서는 관대하지만, 서버 응답은 아니다. 가장 자주 보는 에러가 이거다.

xmllint --xpath '//item/title' feed.xml
feed.xml:1: parser error : Start tag expected, '<' not found
<!DOCTYPE html>
^

XML인 줄 알았는데 HTML이 왔다는 뜻이다. 원인은 대개 셋 중 하나다. (a) 피드 URL이 옮겨졌는데 301이 아니라 홈으로 200 리다이렉트, (b) Cloudflare 챌린지 페이지, (c) User-Agent 없는 요청 거절. curl -IContent-Type부터 확인하는 습관을 들이자.

$ curl -sIL -A "feedbot/1.0" https://example.com/feed | grep -iE '^(HTTP|content-type|location)'
HTTP/2 301
location: https://example.com/feed.xml
HTTP/2 200
content-type: application/rss+xml; charset=UTF-8

2) FeedBurner 경유 URL. 오래된 블로그는 아직도 feeds.feedburner.com/...을 걸어둔 경우가 있다. 원문에 나오듯 구글은 2012년에 API를, 2022년 7월에 이메일 구독을 포함한 대부분의 기능을 정리했다. 이 URL을 파이프라인에 박아두면 언제 끊길지 모르는 SPOF다. 가능하면 원본 사이트의 피드로 갈아타라.

3) 상대 경로 href.grep 예시에서 href="/blog/feed"처럼 상대 경로가 나온다. 자동화에서 그대로 GET 하면 InvalidURL로 죽는다. 반드시 base URL과 join 해야 한다.

4) GUID 신뢰 문제. 어떤 CMS는 글을 수정할 때마다 GUID를 새로 발급한다. 그러면 같은 글이 반복 알림된다. 이럴 땐 GUID 대신 link의 정규화된 형태(쿼리스트링 제거)를 키로 쓰는 게 안전하다. 반대로 pubDate만 믿으면 타임존 없는 값 때문에 하루치가 통째로 재발송되는 사고가 난다.

트레이드오프와 대안

RSS는 폴링 기반이다. 실시간성이 필요한 알림(장애 탐지 같은)에는 부적합하다. 5분 폴링이면 최대 5분 지연이고, 폴링 간격을 줄이면 상대 서버에 부담이다. 실시간이 중요하면 Webhook을, 그게 없으면 RSS를 쓰는 식으로 계층을 나누는 게 맞다.

대안으로는 GitHub Releases API(rate limit·토큰 필요), 벤더 SNS/이메일(파싱 지옥), 유료 SaaS 모니터링 등이 있다. 셋 다 RSS보다 운영 비용이 높다. 반대로 RSS는 제공자가 언제든 끌 수 있다는 게 최대 리스크다. 이 글이 지적하는 바로 그 지점이다. 그래서 나는 중요한 피드는 응답 자체를 아카이빙해두고, 피드가 죽었을 때 알림이 오도록 "N일간 신규 항목 0건" 조건도 같이 건다. 조용히 죽는 게 제일 무섭다.

정리

한 줄 요약: RSS는 기술적으로 멀쩡한데 플랫폼이 발견 경로를 걷어내서 안 보이게 된 것이고, 자동화 관점에서는 여전히 가장 싸고 튼튼한 알림 소스다.

누가 언제 쓰면 되냐면,

  • 쓰기 좋은 경우: 업스트림 릴리즈/보안 공지/상태 페이지 추적, 인증 없이 읽기만 하면 되는 정보, 지연 수 분을 감내할 수 있는 알림.
  • 피해야 할 경우: 초 단위 실시간성이 필요한 장애 알림, 인증이 필요한 사내 데이터, 항목 수가 수만 건인 대용량 동기화.
  • 운영 시 필수: ETag/If-Modified-Since 조건부 요청, GUID 정규화, "피드가 조용히 죽었는지" 감시, FeedBurner 같은 중개 URL 제거.

그리고 한 가지 더. 이 글의 진짜 교훈은 RSS가 아니라 의존성 선택에 있다. 플랫폼이 무료로 얹어주는 편의 기능은 그 회사의 전략이 바뀌는 순간 사라진다. Google Reader가 그랬고, FeedBurner API가 그랬다. 우리 스택에서 "그 회사가 내일 없애도 괜찮은가"를 한 번쯤 점검해볼 만하다. 개방형 프로토콜 위에 직접 얇게 구현해두면, 벤더가 손을 떼도 curl은 계속 돈다.

참고 자료

728x90

+ Recent posts