AI가 코드를 쓰는 시대, 좋은 아키텍처란 무엇일까?
우리가 쓰는 개발 방식과 구조는 대부분 사람이 코드를 읽고 이해하며 운영하는 환경에서 발전해왔다. 지금은 AI가 코드 작성과 유지보수에 참여하는 팀이 빠르게 늘고, 일부 팀에서는 운영 중인 코드도 AI가 수정한다. 그렇다면 사람이 중심이던 환경에서 정립된 좋은 구조의 기준은 지금도 충분할까. 이 질문이 한동안 머릿속을 떠나지 않았다.
특히 레거시를 운영할 때 그렇다. AI로 코드를 고치고 나서 의존도가 높은 지점의 사이드 이펙트가 자주 생긴다는 얘기를 들었다. 직접 통계를 낸 건 아니고 전해 들은 얘기지만, 원인을 짐작하기는 어렵지 않다. 고친 파일은 맞는데 그 파일에 매달린 것들을 안 읽었거나 못 읽었다.
AI는 프로젝트 전체를 머릿속에 담고 있지 않다. 매번 필요한 만큼을 읽어서 컨텍스트에 올린다. 그 컨텍스트에는 한계가 있다. 토큰 한도라는 하드 리밋이 있고, 그보다 훨씬 앞에서도 과제에 따라 정확도가 저하될 수 있다.
질문이 바뀌었다. “이 구조가 읽기 좋은가”가 아니라 “AI가 이 기능을 고치려면 몇 개의 파일을 읽어야 하고, 안 읽은 곳에서 무엇이 깨지는가”다.
이 글은 그 질문을 놓고 정리한 생각과, 실제 프로젝트에서 측정해본 숫자의 기록이다. 결론부터 적으면 판단 기준 한 줄로 압축된다. AI가 특정 기능을 수정할 때, 해당 모듈과 소수의 파일만 읽고 안전하게 끝낼 수 있는가.
컨텍스트는 유한한 자원이다
Anthropic이 정리한 Effective context engineering for AI agents는 컨텍스트를 “수확 체감이 있는 유한한 자원”으로 보는 실무적 관점을 제시한다. 토큰을 하나씩 더 넣을 때마다 모델의 어텐션 예산이 소모된다는 설명이다.
명목상 컨텍스트 윈도우가 커졌다고 그 범위 전체에서 같은 정확도가 보장되지는 않는다. Chroma가 2025년 7월에 낸 Context Rot 리포트는 18개 LLM을 여러 장문맥 과제로 평가했다. 실험 전반에서 입력이 길어질수록 성능이 대체로 저하되거나 더 불안정해졌지만, 하락 폭과 시작 지점은 모델과 과제마다 달랐고 모든 모델이 모든 실험에 포함된 것도 아니다. 별도 연구인 NoLiMa에서도 최소 128K 컨텍스트를 지원한다고 알려진 13개 모델 중 11개가 32K 입력에서 짧은 입력 기준 성능의 절반 아래로 떨어졌다.
표준 full self-attention은 길이 n의 입력에서 토큰 관계를 n² 규모로 계산한다. 다만 이 계산 복잡도 자체가 정확도 저하의 직접 원인으로 입증된 것은 아니다. Anthropic은 많은 관계에 어텐션이 분산되는 문제와 장문맥 학습 데이터의 부족을 가능한 설명으로 든다. 적어도 “다 넣으면 알아서 찾겠지”가 정확도를 보장하지 않는다는 점은 여러 장문맥 평가가 공통으로 보여준다.
Anthropic이 제시하는 대안 중 하나가 just-in-time 방식이다. 데이터를 미리 다 올려두는 대신 파일 경로 같은 가벼운 식별자만 들고 있다가, 런타임에 도구로 필요한 것만 끌어온다. 사람이 데이터베이스를 통째로 외우지 않고 필요할 때 조회하는 것과 같은 구조다. 핵심 정보는 미리 넣고 나머지는 런타임에 찾는 하이브리드도 가능하다.
이 방식이 성립하려면 전제가 하나 필요하다. 필요한 것만 골라낼 수 있어야 한다. 여기에는 폴더 구조뿐 아니라 검색 가능한 메타데이터, 인덱스, 도구와 탐색 휴리스틱이 함께 작용한다. 이 글은 그중 코드 아키텍처가 맡는 부분을 본다.
안 읽은 코드에서 깨진다
에이전트가 도구로 조회하지 못했고 탐색 과정에서도 찾지 못한 코드는 사실상 존재하지 않는 코드와 같다. 여기서 실패 모드가 둘로 갈린다. 하나는 고쳐야 할 파일을 못 찾는 경우, 다른 하나는 고친 파일에 매달린 것들을 모르는 경우다. 앞의 것은 작업이 실패하니 바로 안다. 뒤의 것은 작업이 성공한 것처럼 끝난다.
결합도가 높은 레거시에서 두 번째가 특히 잘 터진다. 함수 하나를 고치는 변경이 그 자체로는 완전히 맞다. 그 함수를 부르는 곳이 일곱 군데인데 그중 세 곳이 컨텍스트 밖에 있다는 게 문제다. 사람이라면 “여기 건드리면 결제 쪽이 위험하다” 같은 감을 오래 일하면서 쌓아둔다. 에이전트에게 그 감은 이번에 읽은 파일 안에만 존재한다.
정보 은닉이 여기서 다시 걸린다. Parnas가 1972년에 쓴 On the Criteria To Be Used in Decomposing Systems into Modules는 모듈을 나누는 기준을 “각 모듈이 무엇을 감추는가”로 잡았다. 어렵거나 바뀔 가능성이 높은 설계 결정을 나열하고, 각 모듈이 그중 하나씩을 숨기게 한다. 인터페이스는 내부를 최대한 드러내지 않도록 고른다. 시스템을 한 번에 한 모듈씩 이해할 수 있게 하는 게 목표다.
“다른 모듈의 내부를 몰라도”라는 조건이 사람에게는 편의였다. 에이전트에게는 전제 조건이다. 모듈이 내부를 잘 감출수록 안 읽고 넘어가도 되는 범위가 분명해진다. 반대로 경계가 새는 구조에서는 안전하게 안 읽어도 되는 범위를 정하기 어려워진다. 더 많이 읽거나, 놓칠 위험을 감수해야 한다.
약 54년 전 원칙이 지금 다시 걸리는 이유는 원칙이 새로워서가 아니다. 어겼을 때의 비용 구조가 바뀌었기 때문이다. 예전에는 경계가 흐려지면 사람이 헤맸고, 헤매면서도 감으로 막았다. 지금은 경계가 흐려지면 에이전트가 토큰을 태우고, 그러고도 안 읽은 곳을 깨뜨린다.
폴더 구조가 곧 탐색 신호다
같은 글에서 파일 계층과 이름 자체가 모델에게 신호로 작동한다는 대목이 인상적이었다. tests 폴더 안의 test_utils.py와 src/core_logic/ 안의 test_utils.py는 이름이 같아도 다른 의미를 갖는다. 모델은 그 배치에서 “이걸 언제 어떻게 써야 하는지”를 읽어낸다.
위치 파악이 중요한 문제라는 것은 성능 지표로도 확인된다. Agentless 논문은 복잡한 에이전트 루프 없이 위치 파악(localization) → 수정 → 검증이라는 3단계로 SWE-bench Lite에서 300문제 중 96개(32.00%)를 해결했고, 당시 오픈소스 접근 중 최고 성능이었다. 문제당 평균 비용은 0.70달러였다.
이후 연구들이 파고든 지점 중 하나가 위치 파악이다. LocAgent는 SWE-bench Lite에서 기존 함수 수정이 없는 사례를 뺀 274문제를 대상으로 파일 Acc@1 77.74%를 기록했다. 같은 표본과 지표를 쓴 FastCode는 86.13%를 보고했고, 별도 설정으로 평가한 SHERLOC은 84.33%를 보고했다. 수치들은 사용 모델과 평가 조건이 달라 하나의 연속된 상승 곡선으로 비교할 수는 없다. 그래도 정확한 위치 파악이 문제 해결의 중요한 전제이자 큰 탐색 비용이라는 점은 공통으로 드러난다. 수정 생성, 진단 품질, 테스트와 패치 선택은 별도의 병목으로 남는다.
이 연구들이 파일명이나 폴더 구조를 사람이 더 잘 설계했을 때의 효과를 직접 실험한 것은 아니다. 다만 Anthropic이 설명한 메타데이터 신호와 위치 파악 연구를 함께 놓으면, 파일명과 폴더 구조로 역할을 예측할 수 있게 만드는 것은 합리적인 실무 선택이다. 어디에 무엇이 있는지가 구조로 드러나면 에이전트가 탐색을 시작할 단서가 늘어난다.
토큰으로 계산해보면 50배 차이였다
여기까지는 원칙이고, 실제로 얼마나 차이가 나는지 궁금해서 진행 중인 프로젝트로 재봤다. 도메인 특화 AI 에이전트를 붙인 서비스고, API는 Bun + TypeScript, 프런트는 Vue다. 아래 나오는 모듈명과 파일명은 일반화했고, 바이트와 줄 수는 실측 그대로다.
api/src 아래에서 /test/ 경로를 제외한 TypeScript 파일 전체가 1,017,880바이트다. 이 안에서 에이전트 v3의 프로덕션 TypeScript 49개가 364,087바이트를 차지한다. 이 모듈에 검색 도구를 하나 추가하는 작업의 진입 컨텍스트로 고른 파일은 네 개다.
| 읽는 범위 | 크기 | 전체 대비 |
|---|---|---|
| API 프로덕션 TypeScript | 1,017,880 B | 1× |
| agent-v3 프로덕션 TypeScript | 364,087 B | 2.8분의 1 |
| 도구 추가의 진입 파일 4개 | 19,120 B | 53분의 1 |
네 개 파일은 모듈 README, 도구 레지스트리, AI SDK 어댑터, 도구 타입 정의다. 실제 구현에서는 새 도구와 유사한 구현이나 테스트를 추가로 찾아볼 수 있다. 19,120바이트는 안전한 전체 작업 범위를 증명한 숫자가 아니라, 작업을 시작하기 위해 먼저 읽을 범위를 잰 숫자다.
토큰으로 환산하면 체감이 더 분명해진다. 정확한 토크나이저를 돌린 값은 아니고 TypeScript 소스 기준 대략 3.5바이트당 1토큰으로 잡은 추정치다.
API 소스 전체 1,017,880 B ≈ 291,000 토큰도구 추가 4개 파일 19,120 B ≈ 5,500 토큰Claude Opus 5의 표준 입력 단가는 100만 토큰당 5달러다. 위 추정 토큰 수가 맞다고 가정하면 전체를 한 번 읽을 때 약 1.46달러, 네 개의 진입 파일은 약 0.03달러다. 이후 요청에서 같은 프롬프트 구간이 캐시에 적중하면 해당 토큰의 읽기 가격은 기본 입력가의 10분의 1로 내려간다. 다만 최초 캐시 쓰기 비용이 별도로 들고, 두 범위에 같은 캐시 적중 조건을 적용할 때만 비율이 유지된다.
비용보다 중요한 건 앞 절의 정확도 문제다. Chroma의 실험 과제는 TypeScript 코드 수정이 아니므로 291,000 토큰이라는 숫자만으로 이 작업이 더 많이 틀린다고 단정할 수는 없다. 다만 장문맥 연구가 공통으로 보여주듯, 관련 없는 입력을 더 넣는다고 정확도가 보장되지는 않고 오히려 불안정해질 수 있다. 전체를 읽히는 선택은 비싸고, 정확도 이득도 불확실하다.
내 프로젝트가 지킨 것과 못 지킨 것
원칙을 정리해놓고 내 코드를 보면 성적표가 갈린다.
지킨 쪽부터. 모듈 경계를 문서가 아니라 테스트로 막아뒀다. v3 모듈이 레거시 모듈을 import하면 테스트가 깨진다.
test('production source가 legacy 모듈을 import하지 않는다', async () => { const violations: string[] = []; for (const file of await sourceFiles(root)) { const source = await readFile(file, 'utf8'); if (/from\s+['"][^'"]*(?:legacy-a|legacy-b)\//.test(source)) { violations.push(file.replace(`${root}/`, '')); } } expect(violations).toEqual([]);});정규식 몇 줄짜리 테스트인데, “의존하지 마세요”라고 적어둔 주석보다 훨씬 잘 작동한다. 사람이든 에이전트든 규칙을 어기면 테스트 실행에서 바로 드러난다.
모듈마다 README도 뒀다. v3 모듈 README에는 API 목록, 히스토리 저장 원칙, 모델 설정이 어느 파일에서 조립되는지, 도구 이름이 뭐가 있는지가 들어 있다. 9,823바이트짜리 파일 하나를 읽으면 이 모듈에서 무엇을 어디서 고쳐야 하는지가 대충 잡힌다. 에이전트 입장에서 이건 소스 49개를 훑는 것보다 훨씬 싼 진입점이다.
못 지킨 쪽. 파일이 크다. 테스트 제외 상위 파일을 뽑아보면 이렇다.
2,067줄 agent-v3/service/agent-v3.service.ts2,024줄 agent-v3-message-list.vue2,007줄 agent-v3/dao/agent-v3-history.dao.ts1,606줄 shared/service/reference.resolver.ts모듈 경계는 그었는데 모듈 안에서 파일이 비대해졌다. 파일 단위로 여는 도구라면 2,067줄짜리 서비스의 함수 하나를 고칠 때도 파일 전체가 컨텍스트에 올라온다. 함수나 줄 범위만 읽는 도구는 이를 피할 수 있지만, 큰 파일 안에서 관련 범위를 찾고 영향도를 판단하는 비용은 남는다. 모듈 단위로 줄여놓은 컨텍스트가 파일 단위에서 다시 커질 수 있는 구조다.
여기서 원래 메모에 적어뒀던 것 하나를 정정해야 한다. OpenAI의 harness engineering 사례에서 100줄 안팎이라는 구체적 기준이 붙는 대상은 소스 파일이 아니라 AGENTS.md다. 에이전트 컨텍스트에 항상 들어가는 진입 문서를 짧게 유지하고 상세는 구조화된 docs/ 디렉터리로 넘긴다. 다만 같은 글은 별도로 소스 파일 크기 제한도 커스텀 린트로 강제한다고 밝힌다. 공개된 것은 제한의 존재까지이고 구체적인 줄 수 기준은 없다.
그러니 OpenAI 사례에서 가져올 수 있는 결론은 “100줄짜리 소스 파일”이 아니라 “파일 크기를 기계적으로 관리한다”까지다. 내 프로젝트에서 어디를 나눌지는 위 표와 실제 변경 작업을 보고 별도로 정해야 한다.
솔직히 나는 AGENTS.md를 그렇게 쓰지 않는다. 이 프로젝트에도 루트에 AGENTS.md가 있긴 한데 1,480바이트짜리고, 내용은 전부 프로젝트 메모리 도구 사용법이다. 코딩 컨벤션도, 아키텍처 설명도, docs/로 가는 지도도 없다. 안 써도 충분히 잘 돌아간다.
왜 그런지 생각해보면 답이 앞에 있다. 모듈이 기능 단위로 갈려 있고, 폴더 이름이 controller, service, dao, tools, llm으로 일정하고, 모듈마다 README가 있고, 경계는 테스트가 막는다. 에이전트가 알아야 할 것의 대부분이 이미 구조에 박혀 있다. 진입 문서가 따로 필요한 상황은 구조만으로는 역할을 예측할 수 없을 때 생긴다.
그러니까 AGENTS.md가 필요 없다는 얘기가 아니라, 순서가 있다는 얘기에 가깝다. 문서는 구조가 말해주지 못하는 것을 대신 말해준다. 구조를 고칠 수 있는 상황이라면 문서보다 구조를 먼저 고치는 편이 낫다. 문서는 쉽게 낡고, 테스트와 린트는 규칙을 어기면 즉시 알려준다. 물론 테스트에 새긴 규칙 자체도 계속 관리해야 한다.
불변식의 중복은 OpenAI 팀의 원칙과 충돌한다
원래 정리해둔 항목 중에 “과도한 공통화로 결합도를 높이기보다 단순한 로직은 약간의 중복을 허용한다”가 있었다. 확인해보니 이 문장 전체가 harness engineering 사례와 반대인 것은 아니다. 충돌하는 지점은 여러 곳에서 어긋나면 안 되는 불변식이다.
Ryan Lopopolo가 소개한 해당 팀의 golden principles에는 “불변식을 중앙화하기 위해 hand-rolled helper보다 공유 유틸리티 패키지를 선호한다”가 명시돼 있다. 별도의 동시성 사례에서 이 팀은 범용 p-limit 계열 패키지 대신 OpenTelemetry 계측과 런타임 요구에 맞춘 자체 map-with-concurrency 헬퍼를 만들었다. 이것은 중복 제거 사례라기보다 에이전트가 저장소 안에서 완전히 읽고 검증할 수 있는 의존성을 선호한 사례다.
강제 방식도 문서에만 맡기지 않았다. 의존 방향은 Types → Config → Repo → Service → Runtime → UI 순서로 고정하고, 커스텀 린터와 구조 테스트로 기계적으로 강제했다. 앞 절의 모듈 경계 테스트와 같은 발상이다.
그러면 중복 허용은 틀린 얘기인가. 한 겹 들어가보면 충돌 지점이 좁다.
OpenAI가 공유 유틸리티로 모으라고 한 대상은 불변식(invariant) 이다. 여러 곳에서 각자 구현하면 서로 어긋나고, 어긋난 걸 나중에야 발견하는 종류의 로직. 동시성 제어, 경계 검증 같은 것들이다. 이건 내가 정리해둔 다른 항목(“핵심 비즈니스·보안·데이터 정합성 규칙은 한곳에서 관리”)과 정확히 같은 말이다.
반면 내가 중복을 허용하겠다고 한 대상은 날짜 포맷팅이나 짧은 변환 함수처럼 어긋나도 티가 나고 고치기 쉬운 로직이다. 이런 걸 공통 모듈로 끌어올리면 모듈 간 의존이 하나 늘어난다. 에이전트가 A 모듈을 고치다가 공통 모듈을 열고, 공통 모듈을 고치면 B와 C가 깨질까 봐 다시 B와 C를 읽는다. 세 줄짜리 함수 하나 때문에 읽어야 할 범위가 모듈 세 개로 번진다.
그래서 기준은 중복이냐 공통화냐가 아니라 틀렸을 때 조용히 넘어가느냐다. 조용히 어긋나는 규칙은 한곳에 모으고 기계로 강제한다. 어긋나면 바로 티가 나는 로직은 모듈 안에 두고 경계를 지킨다.
다만 이건 논문으로 검증된 결론이 아니라 모듈 독립성과 컨텍스트 비용을 같이 놓고 내린 실무 판단이다. 반대 사례를 만나면 바뀔 수 있다.
남은 기준 한 줄
쌓인 원칙을 추리면 다섯 개다.
-
파일명과 폴더 구조로 역할을 예측할 수 있게 만든다. 이름과 배치는 탐색의 중요한 신호다.
-
모듈 경계는 문서에만 맡기지 않고 테스트와 린트로 막는다. 문서와 규칙은 둘 다 관리한다.
-
모듈이 내부를 잘 감출수록 에이전트가 안 읽고 넘어가도 되는 범위가 분명해진다.
-
조용히 어긋나는 규칙은 한곳에 모으고, 그렇지 않은 로직은 모듈 안에 둔다.
-
파일 크기를 방치하면 전체 파일을 읽는 도구의 컨텍스트가 커지고, 부분 읽기 도구에서도 탐색 비용이 늘어난다.
이 전부를 하나로 줄이면 처음에 적었던 질문으로 돌아간다.
AI가 특정 기능을 수정할 때, 해당 모듈과 소수의 파일만 읽고 안전하게 완료할 수 있는가.
이 문장에서 무게가 실리는 단어는 “소수의 파일”과 “안전하게” 두 개다. 앞은 읽는 비용이고 뒤는 안 읽어서 깨지는 비용이다. 낮은 결합도는 대체로 둘을 같은 방향으로 개선하지만, 안전성은 구조만으로 보장되지 않는다. 테스트, 의존 관계 탐색, 런타임 검증이 함께 있어야 한다.
이 질문이 좋은 이유는 적어도 일부를 측정할 수 있기 때문이다. “읽기 좋은 코드”는 사람마다 다르지만 “이 작업을 시작하려면 몇 바이트를 읽는가”는 셀 수 있다. 내 프로젝트에서 진입 컨텍스트의 숫자는 53분의 1이었다. 다만 이 숫자는 컨텍스트 양만 말해준다. 안전성까지 재려면 실제 변경 파일 수, 위치 파악 재현율, 회귀 테스트와 실패율을 함께 봐야 한다. 2,067줄짜리 파일은 그다음 측정 대상이다.
사람이 편하자고 만든 방식이 지금도 맞는지는 방식마다 다르게 답할 문제다. 다만 이제는 답을 감이 아니라 숫자로 낼 수 있게 됐다. 다음에 손볼 곳은 이미 정해진 셈이다.
참고 자료
-
Harness engineering: leveraging Codex in an agent-first world — OpenAI
-
Agentless: Demystifying LLM-based Software Engineering Agents
-
FastCode: Fast and Cost-Efficient Code Understanding and Reasoning
-
SHERLOC: Structured Diagnostic Localization for Code Repair Agents
-
On the Criteria To Be Used in Decomposing Systems into Modules — D.L. Parnas (1972)
-
Context Rot: Evaluating LLM Performance Degradation with Increasing Input Tokens — Chroma