애드블럭 종료 후 사이트를 이용해 주세요.

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 토큰은 돈이다 — 한국어를 위한 LLM 출력 압축 도구 (Scrooge 0편)
    AI 엔지니어링 2026. 6. 10. 13:30
    728x90
    반응형

    1. 도입부 — 왜 이 이야기가 중요한가

    LLM에게 길게 답하지 말라고 시켜본 적 있을 것이다. "간결하게", "불릿으로", "200자 이내로". 이게 단순한 취향 문제가 아닌 이유는, 출력 토큰이 곧 비용이자 지연시간이기 때문이다. 같은 정보를 절반의 토큰으로 전달할 수 있다면, 그건 API 청구서와 응답 속도에 직접 꽂히는 최적화다.

    그래서 "LLM 출력을 압축하자"는 도구들이 등장했다. 그런데 이들을 들여다보다가 한 가지가 걸렸다. 압축의 상당 부분이 영어의 약어 관습, 심하면 한문(Classical Chinese)식 함축에 기대고 있었다. 토큰을 줄이는 영리한 트릭이지만, 그 트릭을 읽어내려면 독자가 그 언어 문화의 소양을 갖고 있어야 한다. 압축된 출력이 누군가에게는 더 읽기 어려워진다는 뜻이다.

    이 글은 그 지점에서 출발해 만든 도구 scrooge(구두쇠, github.com/Kir93/scrooge-mode)의 0화다. 시리즈 전체는 "토큰은 돈이다 — KO-first LLM 출력 압축 도구 scrooge 만들기"이고, 0화부터 5화까지 총 6편으로 이어진다. 이 글에서 당신이 얻어갈 것은 두 가지다.

    • 재정의 하나: 출력 압축을 "영리한 압축 트릭"이 아니라 "accessibility(접근성) 문제"로 보면 설계가 어떻게 달라지는가.
    • 결정 하나: 다국어(i18n)를 나중에 "지원 추가"하는 대신 첫 커밋부터 아키텍처에 깔면 무엇이 쉬워지는가.

    scrooge는 공개 저장소(github.com/Kir93/scrooge-mode)다. 다만 이 글에서는 특정 커밋 해시를 인용하지 않는다 — 해시는 리팩터링되면 깨지고 독자가 따라갈 대상도 아니기 때문이다. 대신 "초기 커밋 단계에서 무엇이 이미 결정되어 있었는가"라는 사실만 근거로 쓴다.


    2. 핵심 개념 — "압축"을 다시 정의하기

    출력 압축이란

    LLM의 답변은 기본적으로 장황하다. 인사하고, 맥락을 복창하고, 친절한 부연을 단다. 출력 압축은 이 장황함을 규칙으로 깎아 같은 의미를 더 적은 토큰으로 만드는 일이다. scrooge의 경우, 사용자가 압축 강도(dial)를 고르면 그에 맞는 규칙 묶음(rule)을 LLM에게 시스템 지시로 주입하는 방식으로 동작한다.

    영감의 출처는 분명히 밝혀둔다. scrooge는 caveman(github.com/JuliusBrussee/caveman, MIT, © Julius Brussee)에서 "출력을 압축한다"는 개념을 빌렸다. caveman은 단순한 "짧게 말하기 팁"이 아니라, 여러 에이전트에서 동작하는 출력 압축 도구로 자신을 설명하며 lite·full·ultra·wenyan(classical Chinese) 같은 모드를 제공한다. scrooge는 코드나 규칙 문구를 그대로 가져온 게 아니라(verbatim copy 아님), 개념만 참고해 i18n-first로 독립 재구현했다. 차이는 뒤에서 설명한다.

    "압축 트릭"과 "접근성"은 다른 문제다

    여기서 이 시리즈를 관통하는 첫 번째 프레임이 나온다.

    기존 도구의 암묵 전제는 대략 이렇다. *"독자는 영어 약어와 함축적 표현을 무리 없이 읽는다."* 영어권 사용자에겐 합리적이다. 하지만 한국어로 일하는 사람에게 영어식으로 압축된 출력은 토큰은 줄었지만 인지 비용은 오히려 올라간 결과물일 수 있다. 압축의 이득이 독자의 모국어 바깥에서 새는 것이다.

    그래서 scrooge의 출발점은 *"어떻게 더 압축할까"가 아니라 *"누구의 토큰을, 누구가 읽을 수 있게 줄이는가"였다. 압축률을 높이는 기술 문제가 아니라, **자기 언어로 토큰을 절약할 수 있게 하는 접근성 문제로 재정의한 것이다. 이름이 "구두쇠(scrooge)"인 이유도 여기 있다 — 토큰 한 톨까지 아끼되, 정작 읽는 사람이 손해 보면 그건 아낀 게 아니다.

    비슷해 보이지만 다른 것

      일반적인 출력 압축 도구 scrooge
    압축의 근거 영어 약어·함축 관습 (암묵 전제) 언어별 규칙을 명시적으로 분리
    다국어 사후에 "지원 추가" 첫 커밋부터 아키텍처에 내장
    문제 정의 토큰 수 최소화 모국어로 토큰 절약 (accessibility)
    언어 추가 비용 코어 수정 동반 rule 파일 1개 + registry 1줄

    3. 동작 원리 — i18n을 "코어"가 아니라 "데이터"로

    재정의가 말뿐이 되지 않으려면 아키텍처가 그걸 강제해야 한다. scrooge의 핵심 결정은 언어를 코드에 하드코딩하지 않고, registry라는 매핑 테이블의 데이터로 다룬 것이다.

    구조는 단순하다. language × dial의 조합 하나가 정확히 하나의 규칙 파일 경로로 1:1 매핑된다. 코어 로직은 "어떤 언어인지"를 모른다. 그저 registry에서 경로를 찾아 해당 규칙을 로드할 뿐이다.

    registry mapping

     

    이 설계의 효과는 언어를 하나 추가할 때 드러난다. 일본어(ja)를 넣고 싶다면:

    1. rules/ja/lite.md, rules/ja/full.md — 규칙 파일을 작성하고
    2. registry에 ja 엔트리를 한 줄 추가한다

    코어 코드 변경은 0이다. 언어가 if-else 분기나 switch 문에 박혀 있었다면 언어를 늘릴 때마다 코어를 건드려야 했겠지만, registry 매핑은 그 결합을 끊는다. 이게 "i18n-first"의 실질적 의미다 — 다국어가 기능이 아니라 구조다.

    이 결정은 사후에 끼워넣은 게 아니다. scrooge의 아주 초기 커밋들이 이미 이 골격을 깔고 있다. 저장소 구조와 i18n 아키텍처를 세운 첫 커밋, 그리고 영어 규칙 골격에서 caveman 특유의 내부자(insider) 표현을 걷어낸 후속 커밋이 그 증거다. 무엇보다 README 최초 버전에는 아예 *"Why Scrooge — positioning is accessibility"라는 섹션이 있다. 즉 "접근성으로서의 압축"은 글을 쓰려고 나중에 붙인 서사가 아니라, *첫날부터 명시된 포지셔닝**이다.


    4. 실무 적용 — registry 매핑이라는 패턴

    이 시리즈는 FE 컴포넌트 코드 대신 설정·구조 예시가 그 자리를 채운다. 0화의 예시는 registry 패턴 그 자체다. 이건 scrooge만의 트릭이 아니라, 다국어·다설정 도구라면 어디든 적용되는 일반 패턴이다.

    ✅ Good Practice — 언어를 데이터로 다루기

    # registry (개념적 표현)
    ko.lite  → rules/ko/lite.md
    ko.full  → rules/ko/full.md
    en.lite  → rules/en/lite.md
    en.full  → rules/en/full.md
    
    # 코어 로직 (언어를 모른다)
    def resolve(language, dial):
        path = registry[f"{language}.{dial}"]   # 매핑 조회
        return load(path)                        # 규칙 로드
    
    # 언어 추가 = 위 매핑에 줄 추가 + 파일 작성. 코어는 그대로.

    핵심은 resolve 함수 어디에도 "ko""en" 같은 리터럴이 없다는 점이다. 코어는 "조회하고 로드한다"만 안다. 언어 지식은 전부 registry라는 데이터에 산다.

    ❌ Anti-Pattern — 언어를 코드에 박기

    # 흔한 실수: 분기문에 언어를 하드코딩
    def resolve(language, dial):
        if language == "ko":
            if dial == "lite": return load("rules/ko/lite.md")
            else:              return load("rules/ko/full.md")
        elif language == "en":
            if dial == "lite": return load("rules/en/lite.md")
            else:              return load("rules/en/full.md")
        # ja를 추가하려면? 이 함수를 또 수정해야 한다.

    왜 문제인가. 언어가 늘 때마다 코어 함수를 수정해야 하고, 그 수정은 곧 회귀(regression) 위험이다. 언어 지식이 코드 곳곳에 흩어지면(파서에 하나, 검증기에 하나, UI에 하나…) "ja 추가"는 그 모든 곳을 빠짐없이 찾아 고치는 작업이 된다. 하나라도 놓치면 버그다.

    🔍 실행 결과 — 무엇이 달라지나

    registry 패턴에서 "ja 추가"는 파일 2개 + 매핑 1줄짜리 작업으로 수렴한다. 코어는 손대지 않으므로 기존 언어(ko/en)가 깨질 위험이 구조적으로 차단된다. 추가 비용이 "언어 수에 비례하는 코드 수정"에서 "언어당 고정된 데이터 추가"로 바뀌는 것 — 이게 i18n-first가 사주는 것이다.


    5. 장단점 및 고려사항

    장점 단점·비용
    ✓ 언어 추가가 코어 수정 없이 가능 (rule 파일 + registry 1줄) ✗ 초기에 추상화 레이어(registry)를 먼저 깔아야 함
    ✓ 언어 지식이 한곳(데이터)에 모여 일관성 유지 ✗ 규칙이 코드가 아닌 외부 파일이라, 규칙-registry 동기화를 따로 강제해야 함
    ✓ "접근성으로서의 압축"이라는 포지셔닝이 구조로 뒷받침됨 ✗ 도구가 한두 언어로 끝날 거면 over-engineering일 수 있음

    실무 팁 — i18n-first가 정당화되는 조건

    • 언어/설정 축이 늘어날 게 거의 확실할 때 registry 패턴은 값을 한다. 한 언어로 끝날 도구라면 분기문이 더 솔직하다.
    • registry는 계약(contract)이다. 규칙 파일을 옮기거나 이름을 바꾸면 같은 변경에서 registry도 동기화해야 한다. 이 동기화를 사람의 기억에 맡기지 말고 검증 게이트로 강제하는 게 좋다. (이 규율이 실제로 무엇을 막았는지는 시리즈 마지막 5화에서 다룬다.)
    • "1:1 매핑"을 깨고 싶은 유혹(한 규칙을 여러 언어가 공유 등)이 오면, 그게 접근성 약속을 깨는지 먼저 따져본다. 언어별로 압축 관습이 다르다는 게 이 도구의 출발점이었음을 기억할 것.

    6. 결론 및 다음 단계

    핵심 3줄 요약

    1. 출력 압축을 "영리한 트릭"이 아니라 "모국어로 토큰을 아끼는 접근성 문제"로 재정의하면 설계 우선순위가 바뀐다.
    2. 그 재정의를 말이 아니라 구조로 강제하려면, 언어를 코드가 아닌 데이터(registry 매핑)로 다뤄 i18n을 첫 커밋부터 깐다.
    3. 효과는 "언어 추가" 순간 드러난다 — 코어 수정 0, 파일 2개 + 매핑 1줄.

    한국어로 토큰을 아껴보고 싶다면 scrooge를 직접 써보는 게 가장 빠르다.

    저장소는 github.com/Kir93/scrooge-mode에 공개돼 있고, 이슈·PR·새 언어 rule 기여 모두 환영한다.

    반응형

    댓글

Designed by Tistory.