note [🌱 거친 메모 · seedling] [⏳ 시점에 민감 · date-sensitive]

전체 점검은 전체를 보지 않는다 — AI 에이전트와 국소 최적화

약 12분
목차 8

나는 요즘 코드 에디터를 쓰지 않는다.

과장이 아니다. 코드를 직접 타이핑하지 않고 자연어로만 서비스를 만들었다. 기능을 말로 설명하면 AI 에이전트가 코드를 쓰고, 테스트를 돌리고, 실패하면 원인을 찾아 다시 고쳤다. 여기서 에이전트는 묻는 말에 답만 하는 챗봇이 아니라, 저장소의 파일을 직접 읽고 명령을 실행하며 코드를 바꾸는 AI다. 프로젝트가 커진 뒤에는 저장소 전체를 주기적으로 점검하는 일도, 거기서 나온 이슈를 처리하는 일도 에이전트에게 맡겼다.

그런데 “왜 이렇게 만들었지?”에 답할 기억은 코드가 불어나는 속도를 따라가지 못했다. 무엇을 반드시 지켜야 하는지, 지금의 판단에는 어떤 근거가 있는지도 흐릿해졌다.

그래서 설계 이유, 반드시 지킬 조건, 판단 근거와 남은 의심을 저장소에 기록하는 규칙을 만들었다. 이 글에서 말하는 불변조건은 “권한 없는 사용자는 데이터를 읽을 수 없다”처럼, 코드를 고쳐도 깨져서는 안 되는 약속이다.

AGENTS.md가 에이전트에게 일하는 방식을 알려 주는 업무 매뉴얼이라면, 새 규칙은 프로젝트가 무엇을 약속하는지, 그 약속을 믿을 근거는 무엇인지 기록하게 한다. 처음에는 AGENTS.md의 확장판쯤으로 여겼다. 막상 써 보니 약속과 의심을 함께 적는 장부에 가까웠다.

계획은 단순했다.

전체를 점검한다. 문제를 찾는다. 고친다. 반복한다. 그러면 언젠가는 수렴한다.

처음에는 정말 그렇게 되는 듯했다. 서비스 중단, 데이터 손상, 권한 우회처럼 심각하고 재현도 가능한 문제가 하나씩 사라졌다. 이 프로젝트에서 P1으로 분류한 문제들이다. 이대로 몇 바퀴만 더 돌면 점검 결과가 깨끗해질 줄 알았다.

그런데 점검표는 비워지지 않았다.

고치고 나면 다른 문제가 나왔다. 그것까지 고치면 처음에는 보이지 않던 경계 조건이 튀어나왔다. 다시 전체 점검을 돌리면 지난번과 겹치지 않는 이슈가 한 묶음 생겼다. 정기검진을 받을수록 건강해지는 게 아니라 진료과가 늘어나는 기분이었다.

확인된 P1은 줄어드는데 이슈는 끝나지 않았다.

이상했다. 충분히 오래 고치면 남은 문제는 0을 향해 가야 하지 않나. 돌이켜 보면 그 기대 밑에는 큰 착각이 깔려 있었다.

나는 에이전트가 전체를 점검하면, 전체를 이해하게 된다고 생각했다.


① 이슈 개수가 곧 품질은 아니다

처음 내 머릿속에 있던 모델은 대략 이랬다.

Bt+1=Bt−FtB_{t+1} = B_t - F_t

BtB_t는 tt회차를 시작할 때 열려 있던 이슈 수이고, FtF_t는 그중 이번 회차에 닫은 이슈 수다. 새 발견도 재발도 없이 매번 하나 이상 닫는다면 목록은 결국 빈다. 장보기 목록에서 산 물건을 하나씩 지워 나가듯, 고정된 목록을 줄여 가는 모델이다.

실제 프로젝트에 맞는 식은 이쪽이었다.

Bt+1=Bt−Ft+Dt+RtB_{t+1} = B_t - F_t + D_t + R_t

DtD_t는 원래 있었지만 이번에 처음 발견한 문제의 수다. RtR_t는 이번 수정이 새로 만든 문제, 곧 회귀의 수다. 장보기에 빗대면, 수박을 담았더니 그 무게에 장바구니 바닥이 찢어져 목록에 없던 ‘새 장바구니’를 적어 넣는 경우다. 중복 보고나 닫힌 이슈의 재개는 생략했다. 이 식은 목록에 무엇이 들어오고 나갔는지를 셀 뿐, 아직 못 찾은 결함까지 세어 주지는 않는다.

기존 이슈·새 발견·회귀는 생겨난 경로가 달라도 목록에서는 똑같이 한 줄을 차지한다. 그래서 이슈가 열 개에서 열두 개로 늘었다는 사실만으로 서비스가 나빠졌다고 말할 수 없다. 반대로 열 개를 닫았다는 사실만으로 좋아졌다고 말할 수도 없다.

내 프로젝트에서는 같은 기준으로 재현하던 P1이 여러 회차에 걸쳐 줄었다. 알려진 실패 경로를 고쳤고, 같은 절차로 다시 확인했을 때도 문제가 생기지 않았다. 그 범위에서는 분명 나아졌다. 다만 우리는 두 가지 일을 동시에 하고 있었다. 이미 찾은 문제를 목록에서 지우는 일과, 계속 바뀌는 시스템에서 다음 문제를 찾는 일이다.

어두운 방에서 손전등을 켜면 물건이 생겨나는 것처럼 보인다. 하지만 생긴 게 아니라 보인 것이다. 손전등이 밝아질수록 방은 오히려 더 어수선하게 보인다.

여기까지만 보면 낙관할 수 있다. 계속 찾으니 더 많이 보였을 뿐이다. 하지만 새 발견만으로 설명되지 않는 항목도 있었다. 회귀 RtR_t였다. 어떤 수정은 한 문제를 없애면서 다른 문제를 만들었고, 새 문제는 주로 수정 범위의 경계에서 생겼다.

② 각각 맞는 수정이, 합쳐서 틀릴 때

왜 하필 경계였을까. 문제가 생기는 자리를 둘로 나눠 보면 이해하기 쉽다. 각 부분 안에서 생기는 문제가 있고, 부분과 부분을 연결할 때 생기는 문제가 있다. 식으로 그리면 다음과 같다. 실제 품질을 측정한 공식이 아니라, 연결부를 빠뜨리지 않기 위한 모형이다.

L(x)=∑iℓi(xi)+∑i<jcij(xi,xj)L(x) = \sum_i \ell_i(x_i) + \sum_{i < j} c_{ij}(x_i, x_j)

L(x)L(x)는 이 모형에서 시스템 전체가 안고 있는 문제의 총량이고, xix_i는 각 부분의 상태다. 첫 번째 합 ∑iℓi\sum_i \ell_i는 로그인 오류나 느린 쿼리처럼 그 부분 안의 문제를 나타낸다. 두 번째 합 ∑i<jcij\sum_{i<j} c_{ij}는 인증과 캐시, 스키마와 배포 순서처럼 두 부분을 연결할 때 생기는 문제다. 셋 이상이 얽히는 경우는 여기서는 생략했다.

강 양쪽에서 다리를 놓아 오는 공사에 빗대 보자. 첫 번째 합은 각자 맡은 반쪽의 결함, 두 번째 합은 가운데서 두 반쪽이 어긋나는 문제에 해당한다. 양쪽 모두 자기 도면대로 지었어도 이음매는 따로 맞춰야 한다.

에이전트에게 이슈 하나를 주면 대개 관련된 작은 영역을 읽고 그 안의 문제를 고친다. 첫 번째 합의 한 항을 줄이는 셈이다. 그런데 그 영역과 바깥을 잇는 연결부에서는 문제가 오히려 커질 수 있다. 안쪽에서 얻은 것보다 바깥에서 잃은 것이 크면, 부분적으로 맞는 수정이 전체로는 틀린 결과를 만든다.

예를 들면 이렇다. API 응답 형식을 통일하면 일부 클라이언트가 의존하던 예외가 사라질 수 있다. 중복 코드를 합치면 서로 달라야 했던 두 흐름이 하나로 뭉칠 수 있다. 보안을 강화하면 백그라운드 작업이 쓰던 권한 경로가 막힐 수도 있다.

수정 하나하나만 펼쳐 놓고 보면 전부 합리적이다. 문제를 재현했고, 원인을 찾았고, 테스트도 통과했다. 코드 리뷰만 보면 오히려 전보다 낫다. 그런데 시스템은 코드 조각만으로 굴러가지 않는다. 그 조각들이 서로 지키기로 한 약속도 함께 작동해야 한다. 그 약속 가운데 상당수는 타입에도, 테스트에도, 문서에도 적혀 있지 않다.

이게 내가 말하는 국소 최적화다.

흔히 국소 최적화라고 하면 눈앞의 성능만 올리다 전체 성능을 망치는 일을 떠올린다. 여기서는 조금 다르다. 원인은 에이전트가 매번 당장 가장 좋아 보이는 수만 두는 탐욕적(greedy) 방식으로 움직인다는 데 있지 않다. 에이전트가 볼 수 있는 범위와 성공 조건이 애초에 작은 영역에 한정돼 있다는 데 있다.

방 하나만 치워 달라는 부탁과 비슷하다. 방 안만 보고 정리하다 보면, 옆방 식구가 늘 이 방에서 꺼내 쓰던 공구함을 ‘안 쓰는 물건’으로 치울 수 있다. 방은 깨끗해지고 부탁받은 일도 끝난다. 하지만 옆방 사정은 처음부터 보이는 범위 밖이었다.

에이전트는 주어진 문제를 푼다. 시스템이 정말 원하는 것이 그 문제의 해결과 같은지는 별개의 문제다.

특히 까다로운 문제는 경계에 산다. 두 모듈 가운데 누가 책임져야 하는지, 지금의 중복이 실수인지 의도된 격리인지, 단순해진 흐름이 미래의 확장성을 갉아먹는지. 코드를 더 읽는 것만으로 답이 정해지지는 않는다. 어떤 시스템을 만들지 선택해야 한다.

③ 전체 점검은 전체를 보지 않는다

“그렇다면 매번 전체 저장소를 읽히면 되지 않을까.”

나도 그렇게 생각했다. 그래서 정기적으로 전체 점검을 시켰다. 아키텍처, 보안, 성능, 테스트, 중복, 죽은 코드, 문서와 구현의 불일치를 훑게 했다. 프롬프트도 굳이 세세하게 쓰지 않았다. 내가 쓴 에이전트는 “전체적으로 꼼꼼히 봐줘”처럼 뭉뚱그린 말도 놀랄 만큼 찰떡같이 알아들었다.

그런데 찰떡같이 알아듣는 것과 전체를 하나로 이해하는 것은 다른 능력이었다.

적어도 내가 돌린 전체 점검은 시스템 전체를 한 번에 이해하는 작업이 아니었다. 보안, 성능, 테스트라는 서로 다른 기준으로 같은 저장소를 여러 번 보는 작업에 가까웠다. 건물 하나를 소방·전기·위생 점검관이 차례로 둘러보는 장면과 비슷하다. 모두 건물 구석구석을 걷지만, 저마다 자기 점검표에 있는 것을 본다.

설명은 투영이다에서 투영이라고 부른 현상과 같다. 그림자가 물체의 윤곽만 남기듯, 파일을 모두 입력하거나 하나씩 순회해도 최종 판단이 모든 맥락을 보존하지는 않는다.

점검 기준잘 찾는 것놓치기 쉬운 것
보안노출, 권한, 입력 검증운영 편의, 복구 흐름
성능병목, 중복 호출, 큰 비용의도적인 격리, 단순성
코드 품질중복, 복잡도, 일관성역사적 이유, 예외의 가치
테스트관측 가능한 실패아직 문장으로 만들지 못한 의도

점검을 여러 개 합치면 사각지대가 줄어든다. 그래도 기준들이 충돌할 때 무엇을 우선할지는 따로 결정해야 한다. 예를 들어 보안 점검은 권한을 좁히라고 하지만, 복구 흐름은 바로 그 권한이 있어야 돌아갈 수 있다.

여기서 “컨텍스트 윈도우가 더 커지면 해결된다”고 생각하기 쉽다. 컨텍스트 윈도우는 모델이 한 번에 보고 다룰 수 있는 분량이다. 관련 코드와 명세를 더 주면 물론 도움이 된다. 그러나 긴 문맥을 다룬 연구에서는 같은 정보도 입력 안의 위치에 따라 활용 성능이 달라졌다.1

정보가 입력에 들어 있는 것과 판단에 안정적으로 쓰이는 것은 다르다. 오픈북 시험에 교과서를 통째로 들고 들어가도, 책 속 내용을 답안에 제대로 활용한다는 보장은 없다. 저장소 전체가 컨텍스트에 들어가도 무엇을 보존할지 정해 두지 않으면, 에이전트는 과제와 프롬프트에서 그 기준을 추론해야 한다.

내게 더 부족했던 것은 입력 공간보다 무엇을 보존할지 정할 기준이었다.

④ 코드 에디터와 함께 버린 것

한 번의 점검은 모든 맥락을 보존하지 못했고, 그 맥락을 다음 점검으로 온전히 넘기지도 못했다. 이 문제를 키운 조건이 하나 있었다. 나는 정말로 코드 에디터를 쓰지 않았다.

처음에는 이게 자연어 코딩의 가장 통쾌한 부분이었다. 파일을 찾거나, 함수 정의로 이동하거나, 타입 오류 밑줄을 따라가거나, 괄호를 맞출 필요가 없었다. 나는 원하는 결과를 말했고 에이전트가 저장소 안을 돌아다녔다.

그런데 에디터를 버리면서 타이핑만 버린 것은 아니었다.

코드를 직접 고치는 사람은 당장 쓸데없어 보이는 것까지 함께 본다. 인증 코드를 찾다가 이상한 캐시 무효화 로직을 보고, 스키마를 바꾸다가 오래된 마이그레이션의 흉터를 보고, 같은 이름의 함수가 세 군데서 조금씩 다르게 쓰이는 것을 기억한다. 지금 하는 작업과 상관없이 우연히 본 것들이 쌓여 머릿속에 프로젝트의 지형을 만든다.

코드 에디터는 입력 도구인 동시에 지도 제작 도구였다.

자연어 인터페이스는 이런 탐색 과정을 요청과 결과 사이에 감춘다. 나는 “로그인 문제를 고쳐줘”라고 말하고 결과를 받는다. 걸어 다니던 동네를 택시로만 오가게 된 것과 비슷하다. 그것도 탈 때마다 기사가 바뀌고, 기사는 내비게이션만 보고 달린다. 목적지에는 더 빨리 닿지만, 골목의 지형은 나에게도 기사에게도 쌓이지 않는다.

에이전트들은 나보다 많은 코드를 읽었다. 그런데 아무도 그 코드를 하나의 긴 경험으로 읽지 않았다.

나 역시 전체를 읽지 않았다. AI는 전체를 파악하고 있으리라 생각했지만, 실제로는 AI도 나도 그렇지 못했다. 코드와 발견한 이슈, 작업 규칙, 의도와 근거는 저장소와 문서 곳곳에 남아 있었다. 하지만 그 기록들을 하나의 맥락으로 이어받는 주체는 없었다.

그렇다고 인간이 답이라는 뜻은 아니다. 사람도 큰 프로젝트에서는 잊고, 자기 영역만 보고, 자기 관점을 전체로 착각한다. 다만 사람 사이에는 문서에 적히지 않은 맥락이 남는다. 회의와 책임, 오래된 실패의 기억, “저 코드는 이상하지만 건드리면 안 된다”는 공포 같은 것들이다.

AI 에이전트로만 개발하니 그런 맥락의 빈자리가 유난히 선명하게 보였다.

⑤ 장부를 쓴다고 전체가 생기지는 않는다

이 빈자리를 일부 채운 것이 앞서 말한 프로젝트 장부였다. 규칙 덕분에 한 번의 작업이 끝나도 프로젝트의 의도가 남았다. “안전하다”거나 “호환된다”는 주장을 테스트 링크 하나로 뭉개지 않고, 주장과 근거를 나눠 기록했다. 예를 들어 “권한 없는 사용자는 데이터를 읽을 수 없다”는 주장 옆에, 그렇게 믿는 근거와 아직 확인하지 못한 경로를 따로 적는 식이다. 알려진 한계와 받아들인 위험도 함께 남겼다.

장부는 이런 기억을 남기는 틀이었다. 다만 기억을 담을 틀이 있다고 모든 것을 기억할 수는 없었다.

규칙이 할 수 있는 것규칙만으로 할 수 없는 것
합의한 의도와 이번에 하지 않을 일을 남긴다올바른 의도를 대신 선택한다
반드시 지킬 조건과 이를 확인할 검사 위치를 기록한다검사를 만들거나 통과를 강제한다
알려진 반론과 남은 위험을 보존한다아직 이름 붙이지 못한 위험을 드러낸다
사람의 판단이 필요한 경계를 표시한다그 판단을 대신 내린다

이미 중요하다고 알아본 불변조건은 문서에 적고 테스트나 제약과 연결할 수 있다. 하지만 아직 아무도 중요하다고 생각하지 못한 성질에는 빈칸조차 생기지 않는다. 여행 짐 목록도 그렇다. 목록은 적어 둔 물건을 챙기는 데 도움이 되지만, 비가 올 줄 몰랐다면 우산 칸은 처음부터 없다.

장부는 기억이 끊기는 일을 줄였지만, 불확실성까지 없애지는 않았다. 아는 것과 “모른다고 아는 것”을 보존하고 검증 근거가 어디까지 닿는지 보여 줄 뿐, 그 바깥의 문제를 찾아 주지는 않는다.

⑥ 약속을 문서에서 검사로 옮기기

여기까지 와서야 세 층의 역할이 분명해졌다.

층답하는 질문하는 일
AGENTS.md에이전트는 어떻게 일해야 하는가작업 절차, 명령, 리뷰 방식을 지시한다
프로젝트 장부무엇을 왜 보존해야 하는가의도, 불변조건, 결정, 근거, 알려진 위험을 기억한다
강제 장치·필수 검사무엇을 어기면 멈출 것인가제약·테스트·CI로 위반한 변경의 병합·배포를 막는다

이 셋은 같은 문서의 짧은 판과 긴 판이 아니다.

예를 들어 AGENTS.md에 “인증 API를 바꿀 때 구버전 호환성을 확인하라”고 적는 것은 작업 지시다. 장부에 호환 기간과 그 이유, 허용한 예외를 남기는 것은 프로젝트의 기억이다. 호환성을 깨뜨린 수정을 실제로 막으려면, 계약 테스트와 마이그레이션 검사를 만들어 병합·배포의 필수 조건으로 걸어 두어야 한다. 계약 테스트는 두 부분이 주고받기로 한 형식을 지키는지 확인하는 테스트다.

문서는 판단을 돕고, 필수 검사는 판단에만 의존하지 않게 한다. 문서가 벽에 붙은 “이 회로에서 전열기 동시 사용 금지” 안내문이라면, 필수 검사는 차단기에 가깝다. 안내문은 읽고 따르는 사람에게만 통하지만, 차단기는 누가 안내문을 읽었는지와 상관없이 회로에 맞춰 둔 한도를 넘는 전류가 흐르면 내려간다. 다만 차단기도 설치할 때 정해 둔 조건에만 반응한다. 필수 검사도 마찬가지다. 합의한 약속을 검사에 정확히 옮긴 범위에서는, 에이전트가 문서를 놓쳐도 위반한 변경을 막을 수 있다.

목표는 매번 모든 에이전트에게 전체를 이해시키는 것이 아니다. 이미 합의한 핵심 경계만큼은 전체 맥락을 처음부터 복원하지 않아도 지키게 만드는 것이다. 물론 아직 이름 붙이지 못한 약속까지 보호해 주지는 않는다. 무엇을 지킬지 선택하는 설계 판단은 여전히 남는다.

실전 메모 — 짧게 시켜도 덜 흔들리게

그렇다고 매번 프롬프트를 작은 명세서처럼 써야 한다는 뜻은 아니다. 나는 여전히 “전체 점검해봐”, “깃허브 이슈 처리하자”, “다음 뭐할까”라고 말하고 싶다. 다만 무엇을 볼지, 어디까지 행동할지, 언제 멈출지는 적어 둔다.

전체 점검 — 진단만

시스템 경계와 핵심 불변조건을 파악한 뒤 기능·보안·데이터·운영·테스트를 점검해. 재현하거나 코드 경로로 입증한 문제만 심각도순 최대 5개 보고해. 수정하지 마.

GitHub 이슈 — 한 건 끝까지

열린 이슈 중 영향이 크고 재현 가능하며 범위가 명확한 1건을 골라 요구사항 확인→재현→수정→관련 테스트·회귀 점검까지 진행해. 범위 확대·제품 판단·외부 변경 전에는 멈춰 보고해.

다음 작업 — 추천만

최근 변경·열린 이슈·실패한 테스트·TODO·알려진 위험을 조사해. 후보 3개를 영향·비용·위험으로 비교해 지금 할 일 1개를 추천하고 실행하지 마.

어디에나 붙이는 꼬리말

조사와 세부 단계는 자율적으로 정해. 비가역 작업·외부 변경·제품 판단 전에는 멈춰 물어봐. 끝나면 결과·검증 근거·남은 위험만 보고해.

이 지시문도 에이전트가 전체를 이해하게 만들지는 않는다. 다만 일의 테두리는 분명하게 그어 준다. 반복되는 원칙은 AGENTS.md에 두고, 개별 요청에는 이번 작업의 경계만 남긴다.


결국 남은 병목

이 프로젝트에서 내가 맡긴 범위만 놓고 보면, AI 에이전트는 구현 속도를 크게 높였다. 그 덕분에 더 어려운 일이 드러났다. 설계의 기억을 이어 가고, 부분 사이의 약속을 지키며, 무엇을 보존할지 결정하는 일이다. 가게로 치면, 계산대를 늘렸더니 이번에는 포장대 앞에 줄이 늘어선 셈이다.

나는 코드 한 줄을 직접 쓰지 않고 서비스를 만들었고, 약속과 근거를 남길 규칙도 만들었다. 둘 다 꽤 잘 작동했다. 그런데도 전체를 아는 주체는 없었다.

합의한 약속은 장부가 기억하고, 그중 기계가 확인할 수 있는 약속은 제약과 필수 검사로 넘겨야 한다. 아직 이름 없는 약속을 발견하고 선택하는 일은 여전히 설계의 몫이다.

이 프로젝트에서 자연어는 꽤 훌륭한 개발 인터페이스였다. 하지만 인터페이스가 좋아졌다고 아키텍처가 저절로 생기는 것은 아니었다.

주석

  1. 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 검색에서 관련 정보의 위치에 따라 성능이 크게 달라진다는 것을 보였다. 코드 에이전트나 이 프로젝트를 직접 평가한 연구는 아니므로, 본문도 “긴 입력이 무용하다”가 아니라 “접근 가능성과 안정적 활용은 같지 않다”는 범위로만 인용했다. ↩