
문제
AI 코드 에이전트(Claude Code 등)는 코드를 탐색할 때 grep으로 파일을 찾고, read로 파일을 연다. read의 단위는 파일이다.
그런데 파일 하나에 함수가 20개 있으면 어떻게 되는가.
CrossError 타입 하나가 필요해서 read
→ 19개의 불필요한 함수가 딸려옴
→ 컨텍스트 오염
“Lost in the Middle”(Stanford, 2024)은 관련 정보가 컨텍스트 중간에 묻히면 LLM 성능이 30% 이상 하락한다고 보고했다. “Context Length Alone Hurts LLM Performance”(Amazon, 2025)는 불필요한 토큰이 공백이어도 성능이 13.9~85% 하락한다고 밝혔다.
연구는 “컨텍스트가 짧을수록 좋다"고 증명했다. 그러나 코드를 구조적으로 쪼개서 필요한 것만 넣는 도구가 없었다.
filefunc가 그 빈자리다. Go 애플리케이션 개발 — 백엔드 서비스, CLI 도구, 코드 생성기, SSOT 검증기 — 을 위한 코드 구조 컨벤션이자 CLI 도구다.
핵심 원칙
파일 하나에 개념 하나. 파일명 = 개념명.
func이든 type이든 interface든 const 묶음이든 동일하다. 이 원칙 하나에서 모든 룰이 파생된다.
# filefunc 없이
read utils.go → func 20개, 19개 불필요. 컨텍스트 오염.
# filefunc
read check_one_file_one_func.go → func 1개. 정확히 필요한 것.
필요한 5-10개를 집는 것보다, 불필요한 290개를 안 여는 게 더 중요하다.
제1시민은 AI 에이전트다
filefunc의 코드 구조는 사람이 아니라 AI 에이전트에 맞춰져 있다.
AI 에이전트는 ls가 아니라 grep으로 탐색한다. 파일이 500개든 1000개든 rg '//ff:func feature=validate' 한 번이면 끝이다. 파일이 많을수록 각 파일이 작고, read 한 번에 딸려오는 노이즈가 줄어들어 오히려 유리하다.
“파일이 너무 많아지지 않나?“라는 질문이 나올 수 있다. 사람에게는 그렇다. 그러나 사람의 불편은 뷰 레이어(VSCode 확장 등)에서 해결한다. filefunc의 구조를 사람에게 맞춰 타협하지 않는다.
탐색 동선이 바뀐다
기존
사용자 요청
→ 뭐가 있는지 몰라서 ls, find
→ 파일 열어보고 구조 파악
→ 관련 파일 찾으러 또 grep
→ 열었더니 func 20개, 대부분 불필요
→ 탐색 비용 > 실제 작업 시간
filefunc
사용자 요청 + 코드북 제공
→ 코드북 보고 grep 쿼리 즉시 구성
→ 파일 20-30개 read (각각 1개념, 전부 유효 컨텍스트)
→ 작업
30개를 read해도 전부 유효한 컨텍스트면 30개가 문제가 아니다. 1개를 read했는데 30개 분량이 딸려오는 게 문제다.
코드북
코드북은 filefunc 설계에서 가장 중요한 위치를 차지한다. 어노테이션 룰보다 코드북이 먼저다. 코드북이 잘 설계되어야 grep 쿼리가 정밀해지고, grep이 정밀해야 read 목록이 깨끗해진다.
# codebook.yaml
required:
feature: [validate, annotate, chain, parse, codebook, report, cli]
type: [command, rule, parser, walker, model, formatter, loader, util]
optional:
pattern: [error-collection, file-visitor, rule-registry]
level: [error, warning, info]
required 키는 모든 //ff:func과 //ff:type 어노테이션에 반드시 존재해야 한다. grep 신뢰성을 보장하기 위해서다 — required 키에는 빈 곳이 없다. optional 키는 관련 있을 때만 사용한다.
코드북은 AI 에이전트의 프로젝트 지도다. 코드북이 없으면 어휘를 모르는 상태로 탐색을 시작한다. 코드북이 있으면 feature=validate, type=rule 같은 정확한 쿼리를 탐색 없이 바로 던진다.
코드북에 없는 값을 어노테이션에 쓰면 ERROR다. 코드북으로 어휘를 정규화하면 빠진 feature, 중복된 type, 애매한 분류가 목록에서 드러난다. 구멍이 보여야 관리가 된다. 코드북 자체도 검증 대상이다 — required에 최소 1개 키, 중복 값 불허, 소문자+하이픈만 허용.
메타데이터 어노테이션
각 파일의 최상단에 어노테이션을 붙인다. body 전체를 read하지 않아도 상단 몇 줄로 메타를 파악할 수 있도록.
//ff:func feature=validate type=rule control=sequence
//ff:what F1: validates one func per file
//ff:why Primary citizen is AI agent. 1 file 1 concept prevents context pollution.
//ff:checked llm=gpt-oss:20b hash=a3f8c1d2
func CheckOneFileOneFunc(gf *model.GoFile) []model.Violation {
| 어노테이션 | 내용 | 필수 |
|---|---|---|
//ff:func | func 파일의 feature, type, control 등 메타 | func 파일 필수 |
//ff:type | type 파일의 feature, type 등 메타 | type 파일 필수 |
//ff:what | 1줄 설명 — 이 함수/타입이 뭘 하는가 | 필수 |
//ff:why | 왜 이렇게 만들었는가 — 사용자의 결정이 근거 | 선택 |
//ff:checked | LLM 검증 서명 (llmc가 자동 생성) | 자동 |
형식은 //ff:key key1=value1 key2=value2. grep/ripgrep으로 즉시 검색 가능하고, 정형화된 key-value로 도구가 파싱할 수 있다. Go의 //go:generate, //go:embed 관례와 동일한 패턴이다.
control — 1 func 1 control
control=은 모든 func 파일에 필수다. 값은 셋 중 하나:
| control | 의미 | depth 제한 |
|---|---|---|
sequence | 순차 실행 | 2 |
selection | 분기 (switch) | 2 |
iteration | 반복 (loop) | dimension + 1 |
Böhm-Jacopini 정리(1966)에 근거한다. 모든 프로그램은 sequence, selection, iteration 세 가지 제어 구조의 조합이다. filefunc는 이를 함수 단위로 강제한다 — 하나의 함수는 하나의 제어 흐름만 가진다.
control=iteration인 함수는 dimension=도 필수다. 순회하는 데이터의 차원을 명시한다. dimension=1이면 평탄한 리스트(depth ≤ 2), dimension ≥ 2면 명명된 타입(struct/interface) 중첩이 필요하다.
filefunc는 control과 실제 코드의 정합성도 검증한다. control=selection인데 switch가 없거나, control=sequence인데 switch나 loop가 있으면 ERROR다.
LLM 탐색 파이프라인
어노테이션이 검색 인덱스와 같은 역할을 한다. 벡터 임베딩 같은 무거운 인프라 없이 동작한다.
1. 구조적 축소 (LLM 불필요, grep)
코드북 기반으로 grep 쿼리 구성
→ 후보 파일 20-30개 추출
2. 메타 판정 (LLM 불필요 또는 초소형)
각 파일 상단 어노테이션만 read
→ name/input/output/what으로 5-10개로 좁힘
3. 정밀 작업 (대형 LLM, 최소 컨텍스트)
5-10개 파일만 full read
→ 코드 수정/생성
단계가 진행될수록 컨텍스트가 줄어든다. 대형 LLM이 투입되는 시점에는 정말 필요한 파일만 남아 있다.
CLI
validate — 코드 구조 룰 검증
filefunc validate # 현재 디렉토리
filefunc validate /path/to/project # 명시적 프로젝트 루트
filefunc validate --format json
프로젝트 루트에 go.mod와 codebook.yaml이 필요하다. 읽기 전용. 위반 시 exit code 1.
chain — 호출 관계 추적
filefunc chain func RunAll # 1촌 (기본)
filefunc chain func RunAll --chon 2 # 2촌 (함께 호출되는 함수 포함)
filefunc chain func RunAll --chon 3 # 3촌 (최대)
filefunc chain func RunAll --child-depth 3 # 하위 호출만
filefunc chain func RunAll --parent-depth 3 # 상위 호출자만
filefunc chain feature validate # feature 전체
실시간 AST 분석. --chon은 관계 거리다. 1촌은 직접 호출/피호출, 2촌은 함께 호출되는 함수까지 포함한다.
기존 go callgraph는 모든 호출을 정적 분석해 수천 노드가 나온다. chain은 같은 feature 안에서만 추적한다. 코드북의 feature가 곧 줌 레벨이다.
llmc — LLM 검증
filefunc llmc # 현재 디렉토리
filefunc llmc --model qwen3:8b
filefunc llmc --threshold 0.9
//ff:what이 func body와 일치하는지 로컬 LLM(ollama)으로 검증한다. 0.0~1.0 스코어, 기본 임계값 0.8. 통과하면 //ff:checked llm=모델명 hash=해시를 자동 기록한다. body가 변경되면 hash가 달라지므로 재검증이 필요하다.
어노테이션 drift의 핵심 문제 — 자연어인 //ff:what은 기계적 검증이 안 된다 — 를 소형 LLM으로 해결한 것이다. 1 file 1 func이라 파일 단위 1:1 대응이 보장되므로 가능한 접근이다.
룰
파일 구조 룰
| 룰 | 위반 시 |
|---|---|
| 파일 하나에 func 하나 (파일명 = 함수명) | ERROR |
| 파일 하나에 type 하나 (파일명 = 타입명) | ERROR |
| 메서드: 1 file 1 method | ERROR |
| init()은 단독 불가 (var 또는 func과 함께) | ERROR |
_test.go는 복수 func 허용 | 예외 |
| 의미적으로 한 묶음인 const는 같은 파일 허용 | 예외 |
코드 품질 룰
| 룰 | 위반 시 |
|---|---|
| nesting depth: sequence=2, selection=2, iteration=dimension+1 | ERROR |
| func 최대 1000줄 | ERROR |
| func 권고: sequence/iteration 100줄, selection 300줄 | WARNING |
nesting depth는 control 유형에 따라 다르다. sequence와 selection은 depth 2. iteration은 dimension + 1 — dimension=1(평탄한 리스트)이면 depth 2, dimension=2(중첩 구조)이면 depth 3. Go의 early return 패턴과 결합하면 대부분의 코드가 이 제한 안에 들어온다.
selection(switch)은 case가 길어지는 경향이 있으므로 권고 줄 수가 300으로 넓다.
.ffignore
프로젝트 루트에 .ffignore를 두면 모든 filefunc 명령어에서 해당 경로를 제외한다. .gitignore와 동일한 문법.
vendor/
*.pb.go
*_gen.go
internal/legacy/
생성된 코드(protobuf, 코드젠 출력)나 외부 벤더 코드처럼 filefunc 룰을 강제할 수 없는 코드를 제외하기 위한 것이다.
whyso와의 연동
func = file이므로 함수 단위 변경 이력이 파일 단위로 정확히 떨어진다.
whyso history check_ssac_openapi.go # CheckSSaCOpenAPI 함수의 변경 이력
한 파일에 함수가 여러 개 있으면 어느 함수가 바뀐 건지 diff를 뒤져야 한다. filefunc면 파일 변경 = 함수 변경. 추적 비용 제로.
암묵적 커플링 검출
whyso coupling check_ssac_openapi.go
같은 요청에 함께 수정된 함수:
check_response_fields.go 8회
check_err_status.go 5회
types.go 4회
명시적 관계가 없는데 coupling 통계에서 자꾸 나오면 숨은 의존성 신호다. 같은 비즈니스 규칙을 다른 각도에서 구현한 함수, interface 없이 암묵적으로 format을 맞추고 있는 것, 버그가 항상 같이 터지는 것.
Go 한정인 이유
Go가 아니면 filefunc 구조화가 쉽지 않다. gofmt가 코드 포맷을 강제하고, early return이 관례이고, 예외가 없고, 패키지 = 디렉토리. 다른 언어로 확장하려면 gofmt 수준의 구조 강제 전략이 필요하다. 이는 filefunc의 범위 밖이다.
적용 대상도 명확하다. 백엔드 서비스, CLI 도구, 코드 생성기, SSOT 검증기. 알고리즘 라이브러리, 저수준 시스템 프로그래밍, 성능 크리티컬 핫패스는 대상이 아니다.
정리
LLM 시대의 코드 구조는 사람의 탐색 편의가 아니라 AI의 탐색 효율에 맞춰져야 한다. filefunc는 그 전환의 첫 걸음이다.
파일 하나에 개념 하나. 코드북으로 어휘를 정규화하고, 어노테이션으로 메타를 부착하고, grep 한 번으로 정확한 파일을 찾는다. read 한 번에 불필요한 코드가 딸려오지 않는다. 컨텍스트 오염 차단은 파일 구조 자체가 해결한다.