AI 코딩 도구가 기존 함수를 또 만들 때 막는 법 2026 — CLAUDE.md보다 오래가는 중복 방지 MCP 체크리스트

AI 코딩 도구가 기존 함수를 또 만드는 문제는 프롬프트 예절 문제가 아니라 코드베이스 조회 절차 문제다. 2026년 4월 기준 Claude Code의 CLAUDE.md memory, MCP resources, MCP tools, settings 권한을 같이 써야 중복 생성을 줄일 수 있다. 핵심은 “새 함수 만들기 전 repo lookup을 강제하고, 실패했을 때 로그를 남기고, 다음 작업자가 같은 실수를 반복하지 않게 하는 것”이다. 말은 근사한데, 실제로는 아주 소박한 문제에서 터진다. 예를 들면 이런 장면이다. AI 코딩 도구에게 “날짜 포맷 함수 하나 만들어줘”라고 시킨다. 도구는 성실하게 formatDate()를 새로 만든다. 그런데 이미 formatDisplayDate(), toLocalDateLabel(), formatDateForUser()가 있다. 이름만 봐도 냄새가 난다. 테스트는 통과한다. PR도 작다. 리뷰어는 바쁘다. 며칠 뒤 같은 로직이 세 군데로 갈라진다. 그때부터 유지보수는 조용히 불어난다. 프로젝트가 크면 이 문제는 더 자주 나온다. AI가 멍청해서만은 아니다. 사람도 모르는 함수를 AI가 갑자기 알아내긴 어렵다. 특히 유틸 함수, 에러 매핑, 날짜 처리, 권한 체크, API 클라이언트 래퍼는 이름이 제각각이라 검색을 한 번 대충 하면 바로 놓친다. 그래서 CLAUDE.md에 중복 만들지 말라고 써두면 되지 않나?라는 처방은 절반만 맞다. CLAUDE.md는 규칙을 기억시키는 데 좋다. 하지만 규칙은 조회가 아니다. 중복 방지는 기억이 아니라 절차다. 여기서 MCP가 들어온다. MCP를 “도구 많이 붙이는 기술”로만 보면 이 글은 재미없다. 이번 글에서 보는 MCP는 repo lookup을 작업 시작 전 의식처럼 강제하는 운영 레이어다. 의식이라고 하니 좀 거창한데, 사실상 “삽 들기 전에 땅속 배관 지도부터 봐라” 정도다. 코드베이스에 배관이 이미 있는데 또 파면 물난리 난다.

왜 CLAUDE.md만으로는 오래 못 가나

Claude Code 공식 문서는 memory 위치를 계층으로 설명한다. 엔터프라이즈 policy, 프로젝트 memory, 사용자 memory, 로컬 프로젝트 memory 같은 식이다. 프로젝트의 CLAUDE.md 또는 .claude/CLAUDE.md는 팀 공유 지침을 담는 위치다. 여기에 “기존 함수를 먼저 찾아라”라고 쓰는 건 당연히 해야 한다. 문제는 그 문장이 실행을 보장하지 않는다는 점이다. AI 코딩 도구는 보통 현재 대화에 들어온 파일과 사용자가 준 지시를 우선 본다. CLAUDE.md에 써 있는 원칙이 있어도 실제 작업 순간에 필요한 심볼, 파일, 호출부가 컨텍스트에 없으면 모델은 추론으로 빈칸을 메운다. 이 추론이 평소엔 생산성이다. 중복 함수 문제에서는 바로 사고 포인트다. 예를 들어 normalizePhoneNumber()가 있는지 확인해야 하는 작업을 보자. 프롬프트에 “전화번호 정규화 함수 만들어줘”라고만 쓰면 모델은 새 구현을 만들 가능성이 높다. CLAUDE.md에 “기존 유틸 재사용”이 적혀 있어도, 모델은 어떤 유틸이 있는지 모른다. 알아야 재사용한다. 그러니 문서 지침은 시작점이고, repo lookup은 실행 조건이다. 이 둘을 헷갈리면 CLAUDE.md가 점점 비대해진다. 처음엔 “중복 금지” 한 줄이었다. 다음엔 “utils 먼저 검색”이 붙는다. 그다음엔 “hooks도 검색”이 붙는다. 그다음엔 “services, lib, shared, common도 검색”이 붙는다. 어느 순간 CLAUDE.md는 작업 지침이 아니라 팀의 후회 모음집이 된다. 후회 모음집, 이름은 멋진데 읽는 사람은 없다. 그래서 오래가는 방식은 다르다. 규칙은 짧게 둔다. 조회는 도구로 만든다. 실패는 로그로 남긴다. 반복되는 실수는 체크리스트로 승격한다. 이 구조가 있어야 새 세션, 새 워커, 새 모델이 들어와도 같은 습관을 유지한다. 특히 여러 AI 워커가 같은 코드베이스에서 움직이면 더 그렇다. 한 워커가 만든 규칙을 다른 워커가 못 읽을 수 있다. 하지만 작업 루트의 MCP lookup 절차와 실패 로그는 훨씬 더 직접적이다.

중복 함수가 생기는 7가지 순간

첫째, 이름이 도메인마다 다를 때 생긴다. 결제 쪽은 formatAmount()라고 부르고, 정산 쪽은 moneyLabel()이라고 부르고, 관리자 화면은 currencyText()라고 부른다. AI는 사용자가 준 단어와 비슷한 이름만 찾다가 기존 함수를 놓친다. 둘째, 파일 위치가 팀 역사와 함께 꼬였을 때 생긴다. 초기에는 src/utils에 있었고, 나중에는 src/shared/lib로 옮겼고, 최근에는 feature folder 안에 작은 헬퍼가 생겼다. 사람도 가끔 못 찾는다. AI가 한 번에 찾으면 그게 더 이상하다. 셋째, 테스트가 중복을 실패로 보지 않을 때 생긴다. 동작만 같으면 테스트는 통과한다. 중복 구현 자체는 보통 테스트 실패를 만들지 않는다. 넷째, 타입이 살짝 다를 때 생긴다. 기존 함수는 Date를 받고, 새 작업은 ISO 문자열을 받는다. 모델은 어댑터를 만들지 않고 새 함수를 만들기 쉽다. 다섯째, 에러 메시지나 edge case가 프롬프트에 없을 때 생긴다. 기존 함수는 빈 문자열, null, timezone, locale을 다 처리한다. 새 함수는 happy path만 처리한다. 코드는 짧아 보이지만 운영에서는 더 위험하다. 여섯째, 세션이 오래되어 컨텍스트가 흐려질 때 생긴다. 앞에서 검색한 결과가 뒤 작업까지 정확히 유지된다고 믿으면 안 된다. 긴 세션에서는 “아까 봤던 것 같다”가 제일 무섭다. 일곱째, 리뷰어가 diff만 볼 때 생긴다. 새 함수 자체는 깔끔하다. 문제는 새 함수가 이미 있던 함수를 대체하지 않는다는 점이다. diff만 보면 깨끗하고, repo 전체로 보면 지저분하다. 이 7가지는 전부 프롬프트 한 줄로 완전히 막기 어렵다. 그래서 작업 전, 구현 중, PR 전 세 단계로 lookup을 넣어야 한다. 그리고 각 단계마다 “찾았다”, “못 찾았다”, “비슷한 게 있었지만 쓰지 않았다”를 남겨야 한다.

repo lookup MCP를 어디에 붙일까

MCP 공식 문서는 resources를 모델에 제공되는 컨텍스트 데이터로 설명한다. resources는 파일, 데이터베이스 스키마, 애플리케이션 정보처럼 URI로 식별되는 자료를 노출할 수 있다. MCP tools는 이름, 설명, input schema를 가진 실행 가능한 기능이다. 도구 결과는 텍스트, 이미지, 오디오, resource link, embedded resource, structured content를 포함할 수 있다. 이 구조를 코드 중복 방지에 맞추면 간단하다. resources는 “현재 repo에서 중요한 코드 지도”다. tools는 “그 지도를 검색하고 후보를 비교하는 손”이다. roots는 “서버가 봐도 되는 작업 영역의 경계”다. MCP roots 문서는 클라이언트가 filesystem root를 서버에 노출해 서버가 접근 가능한 디렉터리 경계를 이해하게 한다고 설명한다. 그러니 repo lookup MCP를 붙일 때 첫 질문은 “어떤 검색 도구를 쓸까”가 아니다. 첫 질문은 “이 도구가 어느 root까지만 보게 할까”다. 중복 방지 도구가 repo 밖의 개인 파일이나 secret까지 보면 목적이 뒤틀린다. AI에게 검색권을 주는 건 좋은데, 창고 열쇠까지 다 주면 청소하다 금고도 연다. 실무에서는 세 가지 구성이 무난하다. 첫 번째는 로컬 코드 검색 MCP다. rg, language server, ctags, AST index, tree-sitter 인덱스를 감싸서 현재 repo 안에서만 검색한다. 두 번째는 GitHub MCP다. GitHub의 공식 MCP Server는 repository management, code search, issue/PR 자동화 같은 기능을 제공한다고 설명한다. 원격 저장소 기준으로 후보 함수를 찾거나 PR 전 검색을 걸 때 유용하다. 세 번째는 문서 MCP다. 내부 API 문서, architecture decision record, coding convention을 resources로 노출한다. 중복 함수 방지에는 코드 검색만큼 문서 검색도 중요하다. 왜냐하면 어떤 함수는 코드보다 문서에 먼저 이름이 정해져 있기 때문이다. 예를 들어 “고객 표시용 금액 문자열은 displayMoney()를 쓴다” 같은 규칙은 구현보다 문서에 있을 수 있다. 이 경우 repo lookup은 코드 검색과 문서 검색을 함께 해야 한다. 한쪽만 보면 반쪽짜리 검색이다.

중복 방지 MCP의 최소 도구 5개

첫 번째 도구는 search_symbol이다. 입력은 query, language, paths, limit 정도면 충분하다. 출력은 파일 경로, 라인, 심볼 이름, 짧은 주변 문맥이어야 한다. 여기서 중요한 건 “코드 전체를 통째로 가져오지 않는 것”이다. 후보를 많이 보여주되 토큰은 작게 써야 한다. 두 번째 도구는 search_semantic이다. 이름이 다를 때를 대비한 의미 검색이다. 예를 들어 “format user visible currency”로 검색하면 moneyLabel, displayAmount, formatKRW 후보가 나와야 한다. 단, semantic search는 환각이 섞이기 쉽다. 그래서 항상 실제 파일 경로와 라인으로 되돌아가야 한다. 세 번째 도구는 read_symbol이다. 검색 결과에서 선택한 심볼의 정의, export 위치, 주요 호출부를 읽는다. 이 도구는 “후보를 봤다”를 “재사용 가능성을 판단했다”로 바꿔준다. 네 번째 도구는 find_callers다. 기존 함수가 어디에서 쓰이는지 확인한다. 호출부가 많으면 재사용 우선순위가 올라간다. 호출부가 하나도 없으면 죽은 코드일 수도 있으니 조심한다. 다섯 번째 도구는 record_lookup_decision이다. 이게 제일 재미없지만 제일 오래간다. 어떤 검색어를 썼고, 어떤 후보를 봤고, 왜 재사용하거나 새로 만들었는지 남긴다. 사람은 기록을 귀찮아한다. AI는 기록을 귀찮아하지 않는다. 그러니 AI에게 기록을 시키면 된다. 기록 포맷은 짧아야 한다. 길면 아무도 안 본다. 짧으면 다음 워커가 보고 산다. 그리고 다음 워커가 산다는 건 팀이 산다는 뜻이다. 개발팀 감동 실화, 하지만 눈물은 CI에서만 흘리자.

체크리스트 1단계: 작업 시작 전 검색

작업 시작 전에는 구현을 금지하고 검색만 시킨다. 이 단계에서 AI가 코드를 쓰기 시작하면 이미 늦다. 프롬프트는 이렇게 간단해야 한다.

먼저 새 코드를 작성하지 말고 repo lookup만 해줘.
목표 기능과 비슷한 기존 함수, 훅, 서비스, 유틸, 타입을 찾아줘.
검색어는 한국어/영어/도메인 용어를 섞어서 최소 5개 사용해줘.
각 후보는 파일 경로, 심볼 이름, 재사용 가능성, 부족한 점으로 정리해줘.
후보가 없다고 판단하면 사용한 검색어와 제외한 경로를 남겨줘.

이 프롬프트의 포인트는 “검색어 최소 5개”다. 한 단어 검색은 너무 약하다. 예를 들어 날짜 표시 기능이면 이런 검색어가 필요하다. format date date label display date localized date toDateString createdAt updatedAt 날짜 표시 시간 포맷 영어와 한국어가 섞인 repo라면 둘 다 검색해야 한다. 도메인 단어도 넣어야 한다. 금융 앱이면 settlement, payout, dividend 같은 단어가 필요하다. 교육 앱이면 student, classroom, attendance 같은 단어가 필요하다. 검색 결과가 나오면 바로 구현하지 않는다. 후보를 표로 만든다.

후보 위치 재사용 가능성 부족한 점 판단
formatDisplayDate src/shared/date.ts 높음 timezone 옵션 없음 어댑터로 재사용
toLocalDateLabel src/features/report/date.ts 중간 report 전용 문구 포함 직접 재사용 보류
formatDateForUser src/admin/utils.ts 낮음 admin locale 고정 참고만

이 표가 있어야 새 함수를 만들더라도 이유가 남는다. 이유 없는 새 함수는 기술 부채다. 이유 있는 새 함수는 설계 선택이다. 둘은 생긴 건 비슷한데, 나중에 리뷰할 때 체감이 완전히 다르다.

체크리스트 2단계: 구현 중 재사용 우선순위

검색 후에는 재사용 우선순위를 정한다. 무조건 기존 함수를 쓰라는 뜻은 아니다. 오히려 무리한 재사용은 더 나쁜 추상화를 만든다. 중복 방지의 목표는 “새 함수 금지”가 아니다. 목표는 “기존 맥락을 모르고 새 함수를 만들지 않기”다. 우선순위는 이렇게 잡는다. 첫째, public API로 이미 쓰이는 shared 함수는 가장 먼저 검토한다. 둘째, feature 내부 함수는 같은 도메인일 때만 재사용한다. 셋째, 테스트가 많은 함수는 재사용 후보로 가산점을 준다. 넷째, 이름은 비슷하지만 정책이 다른 함수는 분리한다. 다섯째, 기존 함수가 edge case를 많이 처리하면 wrapper를 만든다. 여섯째, 새 요구사항이 기존 함수의 책임을 넓히면 변경 전에 호출부를 본다. 일곱째, 호출부가 너무 많으면 동작 변경 대신 새 옵션을 검토한다. 여덟째, 옵션이 늘어나 함수가 괴물이 되면 새 함수가 맞을 수 있다. 아홉째, 새 함수를 만들면 기존 함수와 차이를 주석이 아니라 테스트 이름으로 남긴다. 열째, 최종 diff에 lookup decision을 남긴다. 이 우선순위는 AI에게 그대로 줄 수 있다.

구현 전에 후보 함수 3개를 비교해줘.
가능하면 기존 shared 함수를 wrapper로 재사용해줘.
기존 함수의 동작을 바꿔야 한다면 호출부 영향도를 먼저 요약해줘.
새 함수를 만들어야 한다면 기존 후보와 다른 책임을 테스트 이름에 드러내줘.

이 프롬프트는 모델에게 “새 코드를 잘 짜라”가 아니라 “기존 코드와 관계를 설명하라”고 요구한다. AI 코딩의 품질은 코드 줄 수보다 관계 설명에서 갈린다. 관계를 설명하지 못하는 새 함수는 나중에 이름만 예쁜 중복이 된다.

체크리스트 3단계: PR 전 중복 스캔

PR 전에는 새로 추가된 함수 이름과 비슷한 기존 심볼을 다시 검색한다. 작업 시작 전 검색만으로는 부족하다. 구현 중 이름이 바뀌고 책임이 바뀌기 때문이다. 예를 들어 처음엔 “date formatting” 작업이었는데 구현하다 보니 “relative time label”이 되었을 수 있다. 그럼 처음 검색어는 이미 낡았다. PR 전 스캔은 새 diff 기준으로 해야 한다. 추가된 함수 이름을 뽑는다. 추가된 export를 뽑는다. 추가된 테스트 describe 이름을 뽑는다. 추가된 파일 경로의 도메인 단어를 뽑는다. 이 네 가지를 검색어로 다시 돌린다. MCP tool이 있다면 record_lookup_decision에 결과를 저장한다. 없다면 PR 본문에 짧게 남긴다.

### Duplicate lookup
- New symbols: `formatRelativeDateLabel`, `getRelativeDateText`
- Search terms: relative date, date label, time ago, formatRelative, 날짜 표시
- Reused: `formatDisplayDate()` for absolute fallback
- Not reused: `toLocalDateLabel()` because it is report-specific
- Risk: locale expansion may need shared helper later

이 정도면 충분하다. PR 본문이 논문이 되면 안 된다. 리뷰어가 20초 안에 “아, 찾아봤구나”를 확인하면 된다. AI가 만든 PR은 특히 이 로그가 중요하다. 왜냐하면 리뷰어는 “모델이 정말 찾아봤나?”를 알 수 없기 때문이다. 로그가 없으면 믿음으로 리뷰해야 한다. 믿음 리뷰는 종교고, 우리는 CI를 믿는다.

before 프롬프트: 중복을 부르는 말투

나쁜 프롬프트는 대체로 짧고 친절하다. 예를 들면 이렇다.

사용자 프로필 화면에 생년월일을 보기 좋게 표시하는 함수를 만들어줘.

이 프롬프트는 모델에게 새 함수를 만들라고 사실상 허락한다. 기존 코드를 보라는 말이 없다. 검색 범위가 없다. 재사용 기준이 없다. 후보 비교가 없다. 실패 로그가 없다. 다른 예시도 보자.

금액 표시 유틸 하나 추가해줘. 천 단위 콤마랑 원 표시만 되면 돼.

이 문장은 더 위험하다. 금액 표시는 세금, 통화, 반올림, 음수, null, 로케일이 붙는 순간 정책 함수가 된다. 그런데 “하나 추가”라고 말해버리면 모델은 진짜 하나 추가한다. 그리고 그 함수는 작아서 리뷰를 통과한다. 작은 중복은 통과가 쉽다. 그래서 무섭다. 큰 중복은 티라도 난다. 작은 중복은 조용히 퍼진다. 또 다른 나쁜 프롬프트다.

에러 코드를 메시지로 바꾸는 mapper 만들어줘.

에러 매핑은 거의 항상 기존 정책이 있다. API 에러, 폼 에러, 결제 에러, 인증 에러가 다르게 표현될 수 있다. 이걸 새로 만들면 사용자 메시지가 화면마다 달라진다. 개발자는 “문구 차이”라고 부르지만, 고객은 “서비스가 이상함”이라고 느낀다.

after 프롬프트: repo lookup을 먼저 시키는 말투

좋은 프롬프트는 모델의 손을 잠깐 묶는다. 바로 구현하지 못하게 한다. 대신 검색, 비교, 판단을 먼저 시킨다.

사용자 프로필 화면에 생년월일 표시가 필요해.
코드 작성 전에 repo lookup을 먼저 해줘.
`date`, `birthday`, `birthDate`, `profile`, `format`, `날짜`, `생년월일`로 기존 함수와 호출부를 찾아줘.
기존 함수를 재사용할 수 있으면 wrapper만 만들어줘.
새 함수가 필요하면 기존 후보와 다른 책임을 한 문장으로 남겨줘.

금액 표시 작업은 이렇게 바꾼다.

금액 표시 유틸을 새로 만들기 전에 기존 money/currency/amount formatter를 찾아줘.
검색어는 `money`, `currency`, `amount`, `price`, `KRW`, `원`, `금액`, `콤마`를 사용해줘.
후보 함수의 입력 타입, 반올림 정책, null 처리, locale 처리를 비교해줘.
구현은 비교표를 낸 뒤에만 진행해줘.

에러 매핑 작업은 이렇게 바꾼다.

에러 코드를 사용자 메시지로 바꾸는 로직이 필요해.
먼저 기존 error mapper, toast message, form error, API error handler를 검색해줘.
새 mapper를 만들기 전에 기존 메시지 정책과 충돌하는지 확인해줘.
최종 diff에는 lookup decision을 PR 본문용 bullet로 남겨줘.

이 프롬프트들은 길어 보인다. 하지만 중복 함수 하나가 6개월 뒤 만드는 비용에 비하면 싸다. 프롬프트는 한 번 길고, 부채는 오래 길다. 어느 쪽이 싫은지 고르면 된다.

MCP lookup 결과 포맷

AI에게 검색을 시키면 결과가 예쁘게만 나오고 핵심이 빠질 때가 있다. 그래서 포맷을 고정하는 게 좋다. 나는 아래 포맷을 추천한다.

lookup_id: duplicate-check-2026-04-28-001
task: "사용자 생년월일 표시"
new_symbol_candidate: "formatBirthDateLabel"
search_terms:
  - "birthDate"
  - "birthday"
  - "date label"
  - "profile date"
  - "생년월일"
checked_paths:
  - "src/shared"
  - "src/features/profile"
  - "src/features/account"
matches:
  - symbol: "formatDisplayDate"
    file: "src/shared/date/format.ts"
    decision: "reuse"
    reason: "locale fallback already handled"
  - symbol: "toLocalDateLabel"
    file: "src/features/report/date.ts"
    decision: "do_not_reuse"
    reason: "report-only suffix included"
final_decision: "reuse_with_wrapper"
risk: "birthday timezone policy should be documented"

이 포맷은 사람에게도 좋고 AI에게도 좋다. 다음 세션에서 그대로 붙여 넣어도 맥락이 살아난다. MCP tool이 structured content를 지원한다면 JSON으로 남겨도 된다. MCP tools 문서에서는 output schema를 제공하면 구조화된 결과 검증에 도움이 된다고 설명한다. 그러니 lookup decision은 가능하면 schema를 둔다. 예를 들어 final_decisionreuse, reuse_with_wrapper, new_function, defer 중 하나로 제한한다. risk는 문자열 배열로 둔다. matches는 symbol, file, decision, reason을 필수로 둔다. 이렇게 하면 로그가 쌓였을 때 나중에 분석할 수 있다. 어떤 팀은 중복 함수 자체보다 “검색을 했는데도 왜 놓쳤는가”를 보는 게 더 중요하다. 검색어가 부족했나. 인덱스가 오래됐나. 파일 root가 잘못 잡혔나. 권한 때문에 못 봤나. semantic search가 엉뚱한 후보를 위로 올렸나. 이런 질문이 가능해진다. 로그 없는 AI 워크플로는 감으로 운영된다. 감은 멋있지만, 월요일 아침 장애 회의에서는 인기가 없다.

실패 로그는 이렇게 남긴다

중복이 생겼다면 실패 로그를 남겨야 한다. 비난하려고 남기는 게 아니다. 다음 워커가 덜 헤매게 하려고 남긴다. 실패 로그는 짧고, 재현 가능하고, 검색 가능해야 한다.

## Duplicate Function Incident
- Date: 2026-04-28
- Task: profile birthday display
- Duplicate added: `formatBirthDateLabel`
- Existing function missed: `formatDisplayDate`
- Why missed: searched `birthday` only, did not search `date label`
- Detection point: review
- Fix: replaced implementation with wrapper around `formatDisplayDate`
- New rule: date display tasks must search `date`, `label`, `format`, domain field name

이 정도면 충분하다. 중요한 건 “누가 잘못했나”가 아니라 “어떤 검색어가 빠졌나”다. AI 워커가 여러 명이면 실패 로그는 더 중요하다. Worker A가 놓친 검색어를 Worker B가 다시 놓치지 않게 해야 한다. Worker C는 얌전히 자기 파일만 만들면 된다. 방금 내 얘기 같지만 아무튼 그렇다. 실패 로그는 저장 위치도 정해야 한다. 작은 팀은 PR 본문으로 충분하다. 반복되는 유형은 docs/ai-lookup-log.md 같은 파일로 올린다. 더 성숙한 팀은 MCP resource로 노출한다. 그러면 다음 AI 세션이 “최근 중복 실패 사례”를 직접 참조할 수 있다. 단, 로그에는 secret, 고객 정보, 내부 토큰을 넣으면 안 된다. MCP resources는 컨텍스트로 들어갈 수 있으니 민감정보를 섞으면 위험하다. Claude Code settings 문서는 permissions에서 민감 파일 deny 예시를 제공한다. 중복 방지와 보안은 따로 놀지 않는다. 검색을 잘 시키려다 민감 파일까지 읽게 만들면 생산성 얻고 보안 잃는 이상한 거래가 된다.

settings에서 막아야 할 것

Claude Code settings 공식 문서는 user settings, project settings, local settings, enterprise managed policy를 구분한다. 프로젝트에 공유되는 설정은 .claude/settings.json에 둘 수 있다. 개인 실험은 .claude/settings.local.json에 둘 수 있다. MCP 서버도 project .mcp.json 승인, enabled/disabled 목록, enterprise allowlist/denylist 같은 제어가 있다. 중복 방지 MCP를 운영할 때도 이 구분을 지켜야 한다. 팀 공통 repo lookup MCP는 project scope가 맞다. 개인 실험용 semantic index는 local scope가 맞다. 회사 전체에서 강제할 서버는 enterprise policy가 맞다. 이걸 섞으면 재현성이 깨진다. 한 사람에게만 있는 MCP로 만든 PR은 다른 사람이 재현하기 어렵다. 반대로 팀 공통이어야 하는 lookup 도구가 개인 설정에만 있으면 새 워커가 못 쓴다. 권한도 좁게 잡는다. 중복 방지 MCP는 보통 read 중심이면 충분하다. 코드 검색, 파일 읽기, 호출부 찾기, 로그 기록 정도면 된다. 처음부터 write 권한, issue 수정, PR 생성, 배포 권한까지 붙일 이유가 없다. GitHub MCP Server는 repository, issue, PR, actions 등 넓은 기능을 제공할 수 있다. 그래서 더더욱 toolset을 좁혀야 한다. 중복 방지 목적이면 code search와 file read 중심으로 시작한다. write 기능은 사람이 필요성을 느낄 때 따로 열어도 늦지 않다. MCP 도구는 많을수록 멋있어 보인다. 하지만 운영에서는 적을수록 설명하기 쉽다. 설명하기 쉬운 권한이 오래간다.

repo lookup MCP 흐름 예시

실제 흐름은 이렇게 잡으면 된다. 1단계, 사용자가 작업을 준다. 2단계, AI가 작업 의도를 심볼 후보로 바꾼다. 3단계, search_symbol로 이름 기반 검색을 한다. 4단계, search_semantic으로 의미 기반 검색을 한다. 5단계, read_symbol로 상위 후보를 읽는다. 6단계, find_callers로 사용 범위를 본다. 7단계, 재사용 또는 새 함수 판단을 한다. 8단계, 구현한다. 9단계, diff 기준으로 PR 전 재검색을 한다. 10단계, record_lookup_decision에 남긴다. 이 흐름은 느려 보이지만 실제로는 빠르다. 중복을 나중에 제거하는 비용보다 훨씬 빠르다. 특히 AI 코딩 도구가 빠를수록 이 절차가 더 필요하다. 빠르게 잘못 만들면 부채도 빠르게 쌓인다. 속도는 좋은데 핸들이 없으면 그냥 무섭다. repo lookup MCP는 핸들이다. 브레이크이기도 하다. 가끔은 내비게이션이다. 비유가 세 개나 나왔으니 이쯤에서 멈추자.

새 함수를 만들어도 되는 경우

중복 방지라고 해서 새 함수가 죄인은 아니다. 새 함수가 맞는 경우도 분명히 있다. 첫째, 기존 함수의 책임이 다른 경우다. 예를 들어 관리자 감사 로그용 날짜와 사용자 화면용 날짜는 정책이 다를 수 있다. 둘째, 기존 함수의 입력 타입을 바꾸면 호출부 위험이 큰 경우다. 셋째, 기존 함수가 legacy behavior를 품고 있어 새 요구사항에 맞추면 버그가 나는 경우다. 넷째, 성능 요구가 다른 경우다. 리스트 10만 건에서 쓰는 formatter와 설정 화면 1회 렌더링 formatter는 다를 수 있다. 다섯째, 보안 정책이 다른 경우다. 사용자에게 보여줄 메시지와 내부 로그 메시지는 분리하는 게 맞다. 여섯째, 테스트 가능성을 위해 작은 순수 함수를 새로 뽑는 경우다. 일곱째, 기존 함수가 이름과 다르게 너무 많은 일을 하는 경우다. 이때는 새 함수가 중복이 아니라 탈출구다. 다만 새 함수가 맞는다는 판단도 기록해야 한다. “기존 함수가 별로라서 새로 만듦”은 기록이 아니다. 기록은 이렇게 써야 한다.

새 함수 필요.
`formatDisplayDate()`는 사용자 locale 표시 정책을 포함한다.
이번 작업은 API payload 생성용 UTC date key가 필요하다.
화면 표시 책임과 payload serialization 책임이 달라 분리한다.

이 정도면 리뷰어가 판단할 수 있다. 새 함수를 금지하는 팀은 결국 이상한 공용 함수 하나에 모든 옵션을 붙인다. 그 함수 이름은 대체로 formatSomethingAdvanced()가 된다. 그다음엔 아무도 건드리지 못한다. 중복도 문제지만 괴물 함수도 문제다. 중복 방지의 목표는 함수 수 줄이기가 아니라 책임을 선명하게 만드는 것이다.

팀에 바로 붙이는 운영표

상황 AI에게 시킬 일 MCP 도구 남길 기록
새 유틸 추가 전 이름/의미 검색 search_symbol, search_semantic 검색어와 후보
기존 함수 발견 호출부 확인 read_symbol, find_callers 재사용 판단
후보가 애매함 책임 비교 read_symbol 왜 보류했는지
새 함수 생성 diff 전 재검색 search_symbol 새 함수 필요 이유
PR 작성 lookup 요약 record_lookup_decision PR bullet
중복 발견 실패 로그 record_lookup_decision 누락 검색어

이 표는 팀 규칙으로 붙이기 좋다. CLAUDE.md에는 이 표 전체를 길게 넣지 않아도 된다. 짧게 이렇게만 적어도 된다.

## Duplicate prevention
- Before adding utilities, formatters, mappers, hooks, or service wrappers, run repo lookup.
- Compare existing symbols before implementation.
- Record lookup decision in PR notes or lookup log.
- Prefer reuse or wrapper when responsibility matches.
- Create a new function only when responsibility, policy, or input/output contract differs.

그리고 실제 lookup 절차는 MCP tool description이나 command 문서에 둔다. 이렇게 해야 CLAUDE.md가 낡아도 절차가 남는다. 문서는 낡는다. 도구도 낡는다. 하지만 실행 경로에 들어간 체크는 상대적으로 오래 버틴다. 특히 PR 전 스캔처럼 워크플로에 들어간 체크는 더 오래간다.

FAQ

CLAUDE.md에 길게 쓰면 안 되나?

써도 된다. 하지만 길게 쓴다고 실행되는 건 아니다. CLAUDE.md는 팀의 기본 지침과 판단 기준을 담기에 좋다. 반면 중복 함수 방지는 현재 repo를 검색해야 하는 작업이다. 그래서 짧은 원칙은 CLAUDE.md에 두고, 실제 검색은 MCP tool이나 command로 분리하는 편이 낫다.

MCP 없이도 할 수 있나?

할 수 있다. rg, IDE symbol search, language server, GitHub code search만으로도 충분히 시작할 수 있다. 다만 MCP를 쓰면 AI 코딩 도구가 같은 인터페이스로 검색, 읽기, 기록을 반복할 수 있다. 팀에 여러 도구가 섞여 있을수록 표준화 이점이 커진다.

semantic search는 꼭 필요한가?

처음부터 필수는 아니다. 작은 repo는 이름 기반 검색과 호출부 확인만으로도 효과가 크다. 다만 오래된 코드베이스, 다국어 변수명, 도메인별 별칭이 많은 repo는 semantic search가 도움이 된다. 대신 semantic 결과는 반드시 실제 파일 경로와 코드로 검증해야 한다.

새 함수를 만들면 실패인가?

아니다. 새 책임, 새 정책, 새 입출력 계약이면 새 함수가 맞다. 실패는 새 함수를 만든 것 자체가 아니라 기존 후보를 확인하지 않고 만든 것이다. 중복 방지는 금지가 아니라 판단 기록이다.

실패 로그를 어디에 두면 좋나?

작게 시작하면 PR 본문이 좋다. 반복 패턴이 보이면 docs/ai-lookup-log.md 같은 팀 문서로 옮긴다. 더 나아가면 MCP resource로 노출해 다음 AI 세션이 참고하게 만들 수 있다. 단, 실패 로그에는 secret, 고객 정보, 내부 토큰을 넣지 않는다.

GitHub MCP만 붙이면 해결되나?

아니다. GitHub MCP는 원격 저장소 검색과 PR 흐름에 좋다. 하지만 로컬에서 생성된 미커밋 코드, 빌드 산출물 제외 규칙, monorepo의 실제 root 경계는 별도 고려가 필요하다. 로컬 repo lookup과 원격 repo lookup을 역할별로 나누는 편이 안전하다.

AI가 검색했다고 거짓말하면 어떻게 하나?

그래서 검색 결과 포맷이 필요하다. 검색어, 후보 파일, 라인, 판단 이유를 남기게 하면 빈말이 줄어든다. MCP tool이 structured output을 제공하면 더 좋다. 검증 가능한 로그가 없으면 “검색했다”는 말은 그냥 기분 좋은 장식이다.

공식 출처

마무리

AI 코딩 도구가 기존 함수를 또 만드는 문제는 “모델아 정신 차려”로 해결하기 어렵다. 모델은 정신을 차리는 대신 컨텍스트를 본다. 컨텍스트에 기존 함수가 없으면 새 함수를 만든다. 그러니 오래가는 해결책은 잔소리가 아니라 조회 경로다. CLAUDE.md에는 원칙을 적는다. MCP에는 검색과 읽기와 기록을 맡긴다. PR 전에는 diff 기준으로 다시 검색한다. 중복이 생기면 실패 로그를 남긴다. 이 네 가지만 지켜도 작은 유틸 중복은 꽤 줄어든다. 그리고 무엇보다 다음 AI 워커가 덜 헤맨다. 코드베이스는 기억력이 나쁘다. 그래서 우리는 기억을 문서에 쓰고, 절차를 도구에 넣고, 실패를 로그로 남긴다. 그게 2026년 AI 코딩 워크플로에서 CLAUDE.md보다 오래가는 중복 방지 방식이다.