note [🌱 거친 메모 · seedling] [⏳ 시점에 민감 · date-sensitive]
전체 점검은 전체를 보지 않는다 — AI 에이전트와 국소 최적화
목차 8
나는 요즘 코드 에디터를 쓰지 않는다.
과장이 아니다. 코드를 직접 타이핑하지 않고 자연어로만 서비스를 만들었다. 기능을 설명하면 AI 에이전트가 코드를 쓰고 테스트했다. 실패하면 원인을 찾아 다시 고쳤다. 프로젝트가 커진 뒤에는 저장소 전체를 주기적으로 점검하고, 발견한 이슈도 에이전트에게 맡겼다.
그런데 코드가 늘어나는 동안 설계의 이유와 불변조건, 판단의 근거는 같은 속도로 쌓이지 않았다.
그래서 그 셋에 남은 의심까지 더해 저장소에 기록하는 규칙을 만들었다.
AGENTS.md가 에이전트의 작업 방식을 정한다면, 새 규칙은 프로젝트가 무엇을 약속하고 왜 그것을 믿는지를 기록했다. 처음에는 AGENTS.md의 확장판으로 봤지만, 실제로는 약속과 의심을 함께 적는 장부에 가까웠다.
계획은 단순했다.
전체를 점검한다. 문제를 찾는다. 고친다. 반복한다. 그러면 언젠가는 수렴한다.
처음에는 실제로 그렇게 보였다. 가장 심각한 이슈, 이 글에서 P1이라 부르는 것들이 하나씩 사라졌다. 여기서 P1은 업계 공통 규격이 아니다. 이 프로젝트에서 일관된 심각도 기준으로 분류하고 재현할 수 있었던 최상위 이슈 — 서비스 중단, 데이터 무결성 훼손, 권한 우회처럼 판정 기준이 선명한 문제들이었다. 이대로 몇 바퀴만 더 돌면 점검 결과가 깨끗해질 줄 알았다.
그런데 점검은 깨끗해지지 않았다.
고치고 나면 다른 문제가 나왔다. 그것까지 고치면 처음에는 보이지 않던 경계 조건이 튀어나왔다. 다시 전체 점검을 돌리면 지난번과 겹치지 않는 이슈가 한 묶음 생겼다. 정기검진을 받을수록 건강해지는 게 아니라 진료과가 늘어나는 기분이었다.
관측된 P1은 줄어드는데 이슈는 끝나지 않았다.
이상했다. 문제를 충분히 오래 고치면 문제의 수는 0을 향해 가야 하지 않나. 돌이켜 보면 그 기대에는 큰 착각이 숨어 있었다.
나는 에이전트가 전체를 점검하면, 전체를 이해하게 된다고 생각했다.
① 이슈 개수는 품질 함수가 아니다
처음 머릿속에 있던 모델은 대략 이랬다.
는 회차를 시작할 때 열려 있던 이슈 수이고, 는 그중 이번 회차에 닫은 이슈 수다. 새 발견·회귀·재개가 없고 매번 라면, 는 결국 0에 가까워진다. 고정된 목록에서 항목을 하나씩 지우는 모델이다.
실제 프로젝트의 식은 달랐다.
는 수정 전부터 있었지만 회차에 처음 발견된 문제다. 는 이번 수정으로 생겨 다음 회차까지 관측된 회귀다. 중복 보고·재개·등급 변경은 생략하고, 한 이슈를 둘 이상의 항에 중복해서 세지 않는다고 가정한다. 따라서 이 식은 이슈 목록의 변화를 나타내는 회계식이지, 잠재 결함의 총량을 재는 법칙이 아니다.
기존 이슈·새 발견·회귀는 발생 경로가 달라도 목록에서는 똑같이 한 줄을 차지한다. 그래서 이슈가 열 개에서 열두 개로 늘었다는 사실만으로 서비스가 나빠졌다고 말할 수 없다. 반대로 열 개를 닫았다는 사실만으로 좋아졌다고 말할 수도 없다.
여러 회차에 걸쳐 같은 기준으로 재현 가능한 P1은 줄었다. 이미 알려진 실패 유형을 없앴고, 재현 절차도 더는 실패하지 않았다. 그러나 이것만으로 시스템 전체가 계속 좋아졌다고 말할 수는 없다. 우리는 고정된 문제 목록을 소진한 게 아니라, 움직이는 시스템을 더 정밀하게 관찰하고 있었다.
다만 판정 기준이 선명한 문제가 먼저 닫힌 것은 이 프로젝트에서 관찰한 순서이지, 모든 프로젝트의 법칙은 아니다.
어두운 방에서 손전등을 켜면 물건이 생겨나는 것처럼 보인다. 하지만 생긴 게 아니라 보인 것이다. 손전등이 밝아질수록 방은 오히려 더 어수선하게 보인다.
여기까지만 보면 낙관할 수 있다. 계속 찾으니 더 많이 보였을 뿐이다. 하지만 새 발견만으로 설명되지 않는 항목도 있었다. 회귀 였다. 어떤 수정은 한 문제를 없애면서 다른 문제를 만들었고, 새 문제는 주로 수정 범위의 경계에서 생겼다.
② 각각 맞는 수정이, 합쳐서 틀릴 때
프로젝트 상태를 라고 하자. 부분 의 상태는 , 그 부분의 손실은 , 두 부분 사이의 손실은 다. 방향에 따른 차이는 이 항에 포함하고 셋 이상이 얽힌 경우는 생략하면, 전체 손실을 거칠게 다음처럼 쓸 수 있다.
첫 번째 합은 로그인 오류나 느린 쿼리처럼 각 부분 안에 있는 문제다. 두 번째 합은 인증과 캐시, 스키마와 배포 순서처럼 부분과 부분 사이에서 생기는 문제다.
에이전트에게 이슈 하나를 주면 대개 작은 영역 를 읽고 그 안의 손실을 줄인다. 그러나 와 바깥을 잇는 상호작용 손실은 오히려 커질 수 있다. 안쪽의 개선보다 경계의 손상이 크면, 각각 맞는 수정이 전체로는 틀린 결과를 만든다.
API 응답 형식을 통일하면 일부 클라이언트가 의존하던 예외가 사라질 수 있다. 중복 코드를 합치면 서로 달라야 했던 두 흐름이 하나가 될 수 있고, 보안을 강화하면 백그라운드 작업의 권한 경로가 막힐 수도 있다.
수정 하나하나만 펼쳐 놓고 보면 전부 합리적이다. 문제를 재현했고, 원인을 찾았고, 테스트도 통과했다. 코드 리뷰만 보면 오히려 전보다 낫다. 그런데 시스템은 코드 조각의 합이 아니라 코드 조각 사이에 누적된 약속의 합이다. 그 약속 가운데 상당수는 타입에도, 테스트에도, 문서에도 적혀 있지 않다.
이게 내가 말하는 국소 최적화다.
흔히 국소 최적화라고 하면 눈앞의 성능만 올리다 전체 성능을 망치는 일을 떠올린다. 여기서는 조금 다르다. 에이전트가 탐욕적이어서가 아니다. 에이전트에게 주어진 관측 범위와 성공 조건이 국소적이기 때문이다.
에이전트는 주어진 문제를 푼다. 시스템이 정말 원하는 것이 그 문제의 해결과 같은지는 별개의 문제다.
특히 까다로운 문제는 경계에 산다. 두 모듈 가운데 누가 책임져야 하는지, 지금의 중복이 실수인지 의도된 격리인지, 단순해진 흐름이 미래의 확장성을 갉아먹는지. 코드를 더 많이 읽는다고 답이 나오지는 않는다. 어떤 시스템을 만들고 있는지에 대한 선택이 필요하다.
③ 전체 점검은 전체를 보지 않는다
“그렇다면 매번 전체 저장소를 읽히면 되지 않을까.”
나도 그렇게 생각했다. 그래서 정기적으로 전체 점검을 시켰다. 아키텍처, 보안, 성능, 테스트, 중복, 죽은 코드, 문서와 구현의 불일치를 훑게 했다. 프롬프트도 굳이 세세하게 쓰지 않았다. 내가 쓴 에이전트는 “전체적으로 꼼꼼히 봐줘”처럼 뭉뚱그린 말도 놀랄 만큼 찰떡같이 알아들었다.
그런데 찰떡같이 알아듣는 것과 전체를 하나로 이해하는 것은 다른 능력이었다.
적어도 내가 돌린 전체 점검은 시스템 전체를 한 번에 이해하는 작업이 아니었다. 점검 기준에 따라 같은 저장소를 다른 축에 투영하는 작업에 가까웠다. 모든 파일을 훑더라도 최종 판단에는 각 기준과 관련된 정보만 남았다.
이 현상은 설명은 투영이다에서 다룬 것과 같다. 파일을 모두 입력하거나 순회해도 최종 판단이 모든 맥락을 보존하지는 않는다.
| 점검이 보는 것 | 잘 찾는 것 | 잔차로 밀려나는 것 |
|---|---|---|
| 보안 | 노출, 권한, 입력 검증 | 운영 편의, 복구 흐름 |
| 성능 | 병목, 중복 호출, 큰 비용 | 의도적인 격리, 단순성 |
| 코드 품질 | 중복, 복잡도, 일관성 | 역사적 이유, 예외의 가치 |
| 테스트 | 관측 가능한 실패 | 아직 문장으로 만들지 못한 의도 |
점검을 합치면 잔차가 줄어들 수는 있다. 하지만 자동으로 하나의 전체가 되지는 않는다.
여기서 “컨텍스트 윈도우가 더 커지면 해결된다”고 생각하기 쉽다. 관련 코드와 명세를 더 주면 물론 도움이 된다. 그러나 장문맥 연구에서는 같은 정보도 입력 안의 위치에 따라 활용 성능이 달라졌다.1 정보가 입력에 들어 있는 것과 판단에 안정적으로 쓰이는 것은 다르다. 저장소 전체가 컨텍스트에 들어가도 보존할 기준이 없으면, 에이전트는 과제와 프롬프트에서 그 기준을 추론해야 한다.
결국 문맥의 크기보다 무엇을 보존할지 정하는 일이 중요했다.
④ 코드 에디터와 함께 버린 것
한 번의 점검은 모든 맥락을 보존하지 못했고, 그 맥락을 다음 점검으로 넘기는 과정도 없었다. 이 문제를 키운 조건이 하나 있었다. 나는 정말로 코드 에디터를 쓰지 않았다.
처음에는 이게 자연어 코딩의 가장 통쾌한 부분이었다. 파일을 찾고, 함수 정의로 이동하고, 타입 오류 밑줄을 따라가고, 괄호를 맞추지 않아도 됐다. 나는 원하는 결과를 말했고 에이전트가 저장소 안을 돌아다녔다.
그런데 에디터를 버리면서 타이핑만 버린 것은 아니었다.
사람은 코드를 직접 고치며 쓸데없어 보이는 것을 함께 본다. 인증 코드를 찾다가 이상한 캐시 무효화 로직을 보고, 스키마를 바꾸다가 오래된 마이그레이션의 흉터를 보고, 같은 이름의 함수가 세 군데서 조금씩 다르게 쓰이는 것을 기억한다. 당장 작업과 무관한 이 우연한 노출이 쌓여 머릿속에 프로젝트의 지형을 만든다.
코드 에디터는 입력 도구인 동시에 지도 제작 도구였다.
자연어 인터페이스는 이런 탐색 과정을 요청과 결과 사이에 감춘다. 나는 “로그인 문제를 고쳐줘”라고 말하고 결과를 받는다.
에이전트들은 나보다 많은 코드를 읽었다. 그런데 아무도 그 코드를 하나의 긴 경험으로 읽지 않았다.
나 역시 전체를 읽지 않았다. AI가 전체상을 가지고 있으리라 생각했지만, 실제로는 AI도 나도 그렇지 않았다. 코드와 발견한 이슈, 작업 규칙, 의도와 근거는 각각 저장소와 문서에 남아 있었다. 하지만 그 기록들을 하나의 맥락으로 이어받는 주체는 없었다.
그렇다고 인간이 답이라는 뜻은 아니다. 사람도 큰 프로젝트에서는 잊고, 자기 영역만 보고, 자기 관점을 전체로 착각한다. 다만 회의와 책임, 오래된 실패의 기억, “저 코드는 이상하지만 건드리면 안 된다”는 공포 같은 비정형적 맥락이 사람 사이에 남는다.
AI 에이전트로만 개발하니 이런 비정형적 맥락의 빈자리가 유난히 선명하게 보였다.
⑤ 기억의 형식을 만든다고, 전체가 생기지는 않는다
이 빈자리를 일부 채운 것이 앞서 말한 프로젝트 장부였다. 규칙 덕분에 한 번의 작업이 끝나도 프로젝트의 의도가 남았다. “안전하다”거나 “호환된다”는 주장을 테스트 링크 하나로 뭉개지 않고, 주장과 근거를 분리해 기록했다. 알려진 한계와 받아들인 위험도 함께 남겼다.
장부는 그 기억을 남길 형식이었다. 다만 기억의 형식과 기억의 완전성은 달랐다.
| 규칙이 할 수 있는 것 | 규칙만으로 할 수 없는 것 |
|---|---|
| 승인된 의도와 비목표를 남긴다 | 올바른 의도를 대신 선택한다 |
| 합의한 불변조건과 관련 근거·장치·게이트의 위치를 기록한다 | 장치·게이트를 대신 구현하거나 필수화한다 |
| 알려진 반론과 남은 위험을 보존한다 | 아직 이름 붙이지 못한 위험을 드러낸다 |
| 사람의 판단이 필요한 경계를 표시한다 | 그 판단을 대신 내린다 |
이미 중요하다고 알아본 불변조건은 문서에 적고 테스트나 제약과 연결할 수 있다. 하지만 아무도 아직 중요하다고 생각하지 못한 성질은 빈칸조차 만들지 않는다.
장부는 아는 것과 “모른다고 아는 것”을 보존한다. 이 규칙은 기억의 단절을 줄였지만 불확실성까지 없애지는 않았다. 검증 근거가 닿는 경계를 보여 줄 뿐, 경계 밖의 문제를 찾아 주지는 않는다.
⑥ 약속을 문서에서 검사로 옮기기
여기까지 와서야 세 층의 역할이 분명해졌다.
| 층 | 답하는 질문 | 하는 일 |
|---|---|---|
AGENTS.md | 에이전트는 어떻게 일해야 하는가 | 작업 절차, 명령, 리뷰 방식을 지시한다 |
| 프로젝트 장부 | 무엇을 왜 보존해야 하는가 | 의도, 불변조건, 결정, 근거, 알려진 위험을 기억한다 |
| 강제 장치·필수 검사 | 무엇을 어기면 멈출 것인가 | 제약·테스트·CI로 위반한 변경의 병합·배포를 막는다 |
이 셋은 같은 문서의 짧은 판과 긴 판이 아니다.
예를 들어 AGENTS.md에 “인증 API를 바꿀 때 구버전 호환성을 확인하라”고 적는 것은 작업 지시다. 장부에 호환 기간과 그 이유, 허용한 예외를 남기는 것은 프로젝트의 기억이다. 계약 테스트와 마이그레이션 검사를 만들고 이를 병합·배포의 필수 조건으로 두어야, 호환성을 깨뜨린 수정이 실제로 멈춘다.
문서는 판단을 돕고, 필수 검사는 판단에만 의존하지 않게 한다. 합의한 약속을 검사에 정확히 옮긴 범위에서는, 에이전트가 문서를 놓쳐도 위반한 변경을 막을 수 있다.
목표는 매번 모든 에이전트에게 전체를 이해시키는 것이 아니다. 이미 합의한 핵심 경계만큼은 전체 맥락을 처음부터 복원하지 않아도 지키게 만드는 것이다. 물론 아직 이름 붙이지 못한 약속까지 보호해 주지는 않는다. 무엇을 지킬지 선택하는 설계 판단은 여전히 남는다.
실전 메모 — 짧게 시켜도 덜 흔들리게
그렇다고 매번 프롬프트를 작은 명세서처럼 써야 한다는 뜻은 아니다. 나는 여전히 “전체 점검해봐”, “깃허브 이슈 처리하자”, “다음 뭐할까”라고 말하고 싶다. 다만 무엇을 볼지, 어디까지 행동할지, 언제 멈출지는 남긴다.
전체 점검 — 진단만
시스템 경계와 핵심 불변조건을 파악한 뒤 기능·보안·데이터·운영·테스트를 점검해. 재현하거나 코드 경로로 입증한 문제만 심각도순 최대 5개 보고해. 수정하지 마.
GitHub 이슈 — 한 건 끝까지
열린 이슈 중 영향이 크고 재현 가능하며 범위가 명확한 1건을 골라 요구사항 확인→재현→수정→관련 테스트·회귀 점검까지 진행해. 범위 확대·제품 판단·외부 변경 전에는 멈춰 보고해.
다음 작업 — 추천만
최근 변경·열린 이슈·실패한 테스트·TODO·알려진 위험을 조사해. 후보 3개를 영향·비용·위험으로 비교해 지금 할 일 1개를 추천하고 실행하지 마.
어디에나 붙이는 꼬리말
조사와 세부 단계는 자율적으로 정해. 비가역 작업·외부 변경·제품 판단 전에는 멈춰 물어봐. 끝나면 결과·검증 근거·남은 위험만 보고해.
이 지시문도 에이전트가 전체를 이해하게 만들지는 않는다. 다만 무엇을 보고, 어디까지 움직이며, 어디서 멈출지를 분명하게 만든다. 반복되는 원칙은 AGENTS.md에 두고, 개별 요청에는 이번 작업의 경계만 남긴다.
결국 남은 병목
이 프로젝트에서 내가 맡긴 범위만 놓고 보면, AI 에이전트는 구현 속도를 크게 높였다. 그 덕분에 더 어려운 일이 드러났다. 시스템의 공유된 기억을 만들고, 부분 사이의 약속을 지키며, 무엇을 보존할지 결정하는 일이다.
나는 코드 한 줄을 직접 쓰지 않고 서비스를 만들었고, 약속과 근거를 남길 규칙도 만들었다. 둘 다 꽤 잘 작동했다. 그런데도 전체를 아는 주체는 없었다.
장부는 합의한 약속을 기억하고, 기계화할 수 있는 약속은 제약과 필수 검사로 넘겨야 한다. 아직 이름 없는 약속을 발견하고 선택하는 일은 여전히 설계의 몫이다.
이 프로젝트에서 자연어는 꽤 훌륭한 개발 인터페이스였다. 하지만 인터페이스가 좋아졌다고 아키텍처가 저절로 생기는 것은 아니었다.
주석
-
Liu et al., “Lost in the Middle: How Language Models Use Long Contexts”, Transactions of the Association for Computational Linguistics 12 (2024), 157–173. 다중 문서 질의응답과 key-value 검색에서 관련 정보의 위치에 따라 성능이 크게 달라짐을 보였다. 코드 에이전트나 이 프로젝트를 직접 평가한 연구는 아니므로, 본문도 “긴 입력이 무용하다”가 아니라 “접근 가능성과 안정적 활용은 같지 않다”는 범위로만 인용했다. ↩