AWS 기술 블로그
십만 줄 코드베이스, AST로 길을 찾다 — Kiro Code Intelligence로 대규모 코드베이스를 이해하는 법
들어가며
새 코드를 작성할 때 AI 코딩 어시스턴트는 충분히 쓸 만합니다. 반면 기존 코드, 그중에서도 수년간 누적된 대규모 레거시를 다룰 때는 사정이 다릅니다. 결제 로직의 수정 시작점을 묻거나, 특정 메서드 이름을 바꿨을 때의 영향 범위를 묻거나, 방금 생성한 코드가 기존 아키텍처 규칙에 맞는지 물으면, 그럴듯하지만 실제 구조와 어긋난 답을 받는 경우가 많습니다.
생성은 잘하면서 이해에서 약한 이유는 여기에 있습니다. 대부분의 AI 코딩 도구는 코드를 텍스트로 취급하고, 코드의 구조는 보지 않습니다.
이 글은 그 한계를 짚고, 코드를 텍스트가 아니라 AST(Abstract Syntax Tree, 추상 구문 트리)로 다루는 접근의 차이를 설명합니다. 이어서 Kiro의 Code Intelligence가 이를 어떻게 구현하는지, 그리고 작은 예제부터 십만 줄 규모의 공개 레포지토리까지 어떻게 활용하는지 다룹니다.
1. AI가 코드를 못 읽는 이유
LLM은 학습하지 않은 코드베이스를 알지 못합니다. 그래서 “새롭게 만든 답변이 기존 코드와 맞는가”를 판단하지 못하고, 모르는 부분을 그럴듯하게 지어내는 할루시네이션이 발생합니다. 이를 줄이고 기존 코드를 답변의 근거로 삼기 위해, AI 코딩 도구는 관련 코드를 찾아 모델의 컨텍스트에 넣어주는 방식을 씁니다.
그 검색 방식으로 널리 쓰이는 것이 RAG(Retrieval-Augmented Generation), 즉 벡터 검색입니다.
코드 → 청킹 → 임베딩 → 벡터DB → 유사도 검색 → LLM
코드를 조각내 임베딩하고, 질의가 들어오면 텍스트가 유사한 조각을 찾아 모델에 전달합니다. 검색 근거를 함께 제공하므로 할루시네이션을 어느 정도 억제하고, 자연어 문서 검색에는 잘 맞습니다. 그러나 대상이 코드일 때는 네 가지 한계가 드러납니다.
| 한계 | 결과 |
|---|---|
| 구조·관계 소실 | “A가 B를 호출한다”, “이 클래스가 저 인터페이스를 구현한다”가 임베딩에 남지 않음 |
| 할루시네이션 | 텍스트 유사성은 실제 의존성과 다름. 무관한 코드를 근거로 끌어옴 |
| 경로 추적 불가 | 입력에서 저장까지 이어지는 호출 흐름을 재구성하지 못함 |
| 문맥 휘발 | 세션 종료와 함께 파악한 맥락이 사라짐 |
코드를 벡터화하는 작업 자체의 비용도 작지 않습니다. 청킹 단위 결정, 변수명과 구조에 의미가 분산된 코드의 임베딩 품질, 변경분 재임베딩 파이프라인, 벡터 DB 운영까지 갖춰야 제대로 동작합니다.
이 부담 때문에 실제 에이전트는 grep과 find로 코드베이스를 매 세션 처음부터 훑는 경우가 많습니다. 수만 줄 규모에서 전체 스캔은 느리고, 그 결과가 매번 모델의 컨텍스트로 올라가면서 같은 탐색이 세션마다 반복되어 토큰을 낭비합니다.
결국 병목은 모델의 성능이 아니라, 모델에 코드베이스의 구조를 전달할 수단이 없다는 데 있습니다. 텍스트 유사도는 좌표만 알려줄 뿐, 좌표 사이를 잇는 길은 알려주지 않습니다.
2. AST: 문법 구조로 코드를 읽기
코드의 구조를 모델에 전달하는 방법은 새로운 것이 아닙니다. 컴파일러와 IDE가 오래전부터 써온 방식이 바로 AST입니다.
IDE가 이미 쓰는 기술
IDE의 핵심 기능 대부분이 AST 위에서 동작합니다.
| 기능 | 동작 |
|---|---|
| 정의로 이동(Go to Definition) | 심볼의 선언 노드로 이동 |
| 모든 참조 찾기(Find All References) | 해당 식별자의 모든 참조를 수집 |
| 안전한 이름 변경(Rename Symbol) | 동일 심볼만 선택적으로 변경 |
| 파일 개요(Outline) | 클래스·함수 트리 = AST 그 자체 |
grep 치환은 주석과 문자열에 든 같은 문자열까지 바꾸지만, IDE의 Rename은 동일 심볼만 바꿉니다. 이 차이가 AST에서 나옵니다.
동작 원리: 코드에서 트리로, 트리에서 관계로
소스 코드는 파서를 거쳐 트리가 됩니다. 변수명·공백·주석 같은 표면 요소는 제거되고 구조만 남습니다.
class PaymentService(PaymentGateway): # implements
def charge(self, card):
validate_card(card) # calls
return db.save(card) # calls
파서가 만드는 트리는 다음과 같습니다.
ClassDef: PaymentService
├─ bases: [PaymentGateway]
└─ FunctionDef: charge
├─ Call: validate_card
└─ Call: db.save
트리를 순회하면 노드 사이의 관계가 엣지로 추출됩니다.
PaymentService ──implements──▶ PaymentGateway charge ──calls───────▶ validate_card charge ──calls───────▶ db.save
이 과정에 LLM은 개입하지 않습니다. 파서가 트리를 만들고 순회가 관계를 뽑습니다. 결과적으로 세 가지 특성을 갖습니다.
- 결정론적: 같은 코드는 항상 같은 그래프를 만듭니다. 변수명이나 주석을 바꿔도 구조는 그대로입니다.
- 파싱은 무비용: 트리를 만들고 관계를 뽑는 과정 자체에는 LLM이 개입하지 않아 토큰이 들지 않습니다. (뽑아낸 결과를 모델의 컨텍스트에 넣어 활용할 때는 그 내용만큼 토큰을 씁니다.)
- 관계 보존:
import·calls·implements가 명시적 엣지로 남아, 호출 경로를 그래프 순회만으로 추적할 수 있습니다.
동작 도구: tree-sitter
언어마다 문법이 다른데 한 도구로 여러 언어의 AST를 다루는 방법이 tree-sitter입니다. 하나의 프레임워크로 다수 언어를 동일한 방식으로 파싱합니다. AST가 개념이라면 tree-sitter는 그 개념을 여러 언어에서 구현한 도구이며, 다음 장에서 다룰 Kiro Code Intelligence의 한 축입니다.
3. Kiro Code Intelligence: 두 레이어로 접근하는 이유
AST를 실제로 쓸 때는 적용 방식을 골라야 합니다. tree-sitter만으로 빠르게 구조를 훑는 방법과, 언어 서버(LSP)를 띄워 타입까지 정확히 분석하는 방법은 비용과 정밀도가 다릅니다. 전자는 설치 없이 바로 쓸 수 있지만 구문 수준의 검색에 그치고, 후자는 타입·참조까지 정확하지만 언어별 서버 설치와 인덱싱 비용이 듭니다.
코드 이해 도구를 처음 붙일 때는 대개 이 설치 비용에서 막힙니다. 정확하지만 무거운 방식만 있으면, 일단 구조부터 훑어보고 싶은 대다수 상황에서 손이 잘 가지 않습니다. Kiro의 Code Intelligence는 이 둘을 하나의 도구 안에 두 레이어로 담아 상황에 맞게 고를 수 있게 했습니다.
가볍게 시작하기 — tree-sitter
첫 번째 레이어는 설정이 필요 없습니다. 앞에서 언급한 tree-sitter가 그대로 동작하므로, 레포지토리를 열면 바로 심볼을 검색하고 파일 구조를 들여다볼 수 있습니다. 언어 서버 설치도, 인덱싱 대기도 없습니다. Java, TypeScript, Python, Go, Rust 등 18개 언어를 같은 방식으로 다룹니다.
이 레이어에서 특히 유용한 것이 Pattern search와 Pattern rewrite입니다. 텍스트가 아니라 구문 트리를 기준으로 코드를 찾고 바꾸는 기능입니다. 특정 호출 형태를 찾되, 인자가 무엇인지는 상관없이 잡고 싶을 때가 있습니다. 이를 위해 패턴 안에서 “아무 노드나”를 뜻하는 메타변수를 씁니다. 메타변수 $VAR는 노드 하나를, $$$는 노드 여러 개를 매칭합니다. 예컨대 someMethod($ARG)라는 패턴은 인자 종류와 무관하게 그 호출 형태만 정확히 잡아냅니다. 정규식이 주석이나 문자열 속 같은 글자까지 끌어오는 것과 달리, 실제 코드 구조만 대상으로 삼습니다. 이 점은 뒤의 리팩터링 사례에서 다시 다룹니다.
정밀도를 더하기 — LSP
타입까지 정확히 봐야 하는 상황, 예컨대 “이 심볼의 모든 참조를 빠짐없이” 찾거나 타입 오류를 확인해야 할 때는 두 번째 레이어인 LSP(Language Server Protocol)를 켭니다. 언어 서버가 심볼·타입·참조의 인덱스를 유지하므로, tree-sitter의 근사 검색보다 정확합니다. 정의로 이동(go to definition), 참조 찾기(find references), 안전한 이름 변경(Rename Symbol), 시그니처 조회(hover)가 이 레이어에서 나옵니다.
대신 언어 서버는 아래 LSP 서버 목록을 참고하여 직접 설치해야 합니다.
| Language | Extensions | Server | Install Command |
|---|---|---|---|
| TypeScript/JavaScript | .ts, .js, .tsx, .jsx |
typescript-language-server |
npm install -g typescript-language-server typescript |
| Rust | .rs |
rust-analyzer |
rustup component add rust-analyzer |
| Python | .py |
pyright |
pip install pyright |
| Go | .go |
gopls |
go install golang.org/x/tools/gopls@latest |
| Java | .java |
jdtls |
brew install jdtls (macOS) |
| Ruby | .rb |
solargraph |
gem install solargraph |
| C/C++ | .c, .cpp, .h, .hpp |
clangd |
brew install llvm (macOS) or apt install clangd (Linux) |
| Kotlin | .kt, .kts |
kotlin-language-server |
brew install kotlin-language-server |
설치 이후 /code init을 실행하면 .kiro/settings/lsp.json이 생성되고 서버가 기동됩니다. 워크스페이스 단위 설정이라, 프로젝트마다 필요한 언어만 켜면 됩니다.
앞에서 본 grep보다 정밀한 단계가 tree-sitter라면, LSP는 거기서 한 단계 더 들어간 컴파일러 수준의 정밀도라고 볼 수 있습니다. 가벼움이 필요하면 tree-sitter, 정확함이 필요하면 LSP를 쓰되, Kiro 안에서 별도 설정 없이 두 레이어를 오갑니다.
프로토콜이 아니라 자연어로
두 레이어 모두, 사용자가 프로토콜을 직접 다루지 않습니다. 예시로, “Owner 클래스를 참조하는 곳을 찾아줘”라고 물으면 Kiro가 이를 내부 구조 질의로 변환하고 결과를 정리해 돌려줍니다. 에이전트도 같은 경로로 정확한 구조 정보를 받아 코드 생성과 수정에 활용합니다. AST의 정밀함을 얻기 위해 트리 순회 코드를 짤 필요가 없다는 뜻입니다.
코드베이스 단위 작업은 몇 개의 /code 명령으로 묶여 있습니다. /code overview로 구조 개요를, /code summary로 AGENTS.md나 README 같은 문서를 생성하고, /code init으로 LSP를 켭니다(/code status로 상태, /code logs로 로그 확인).
4. 실전: 작은 예제에서 대규모 코드베이스까지
코드 이해 도구는 규모가 커질수록 효과가 분명해집니다. 그래서 두 단계로 확인하겠습니다. 먼저 구조가 깨끗한 소형 예제 Spring PetClinic으로 동작 원리를 보고, 같은 방식을 10만 줄 규모의 Apache Commons Lang에 적용해 스케일에서도 그대로 통하는지 확인합니다.
아래 명령과 출력은 흐름을 설명하기 위한 예시이며, 실제 환경·버전에 따라 결과가 다를 수 있습니다.
4-1. 원리 확인 — Spring PetClinic (약 1,800줄)
PetClinic은 owner, vet, model, system 네 패키지로 나뉘고, 각 도메인이 Controller부터 Repository, Entity까지 이어지는 전형적인 레이어드 구조를 갖습니다. 작지만 구조가 또렷해서, Code Intelligence가 무엇을 하는지 보기에 적합합니다.
처음 받은 레포에서 가장 먼저 하는 일은 전체 그림을 잡는 것입니다.
4-2. 스케일 확인 — Apache Commons Lang (약 10만 줄, 263개 파일)
Commons Lang은 수많은 프로젝트가 의존하는 유틸리티 라이브러리입니다. StringUtils 한 파일이 9천 줄을 넘고 public static 메서드만 240개에 달합니다. 이 정도면 직접 읽어 전체를 파악하기는 어렵습니다.
Overview로 구조 파악하기
10만 줄짜리 레포를 처음 열어도 개요는 즉시 나옵니다.
/code overview

텍스트 검색과의 차이, 심볼 검색의 정확성
단순한 검색 요청 시에는 Kiro를 비롯한 대부분의 코딩 어시스턴트가 키워드 검색 도구로 grep을 선택합니다. grep은 isEmpty 라는 문자열이 쓰인 모든 곳을 Commons Lang 레포지토리에서 탐색하며 메서드 정의부와 호출부 모든 곳을 찾게 됩니다.
isEmpty 검색해줘

isEmpty 는 69개 파일, 379회 사용되었다는 결과를 얻었습니다. isEmpty라는 이름의 메서드가 StringUtil, ArrayUtils 등 여러 클래스에서 등장합니다. 결과에는 isEmpty 에 대한 정의와 호출이 구분되지 않고, 클래스에 따라 다른 목적으로 작성된 경우 이를 구분하지 않고 뒤섞어 보여줍니다. 이 과정에서 grep 호출에는 토큰이 소모되지 않지만, 그 결과를 해석하는 과정에서 토큰이 소모됩니다. 만일 나의 의도가 isEmpty의 정의부를 찾고 싶었다면 불필요한 토큰 소모로 이어진 샘입니다.
Kiro Code Intelligence 가 제공하는 심볼 검색(Symbol Search)은 의도에 따라 정의를 바로 찾아올 수 있습니다.
isEmpty 찾아줘

심볼 검색 결과는 grep 문자열 검색과 다른 동작을 보입니다. 메서드 정의를 찾고 어떻게 구현되어 있는지를 설명하고 있습니다.
정교한 탐색을 위한 패턴 검색
패턴 검색(Pattern Search)은 마치 정규식과 같이 더 정교한 탐색을 할 수 있습니다. 메타변수는 AST 노드와 매칭하는 와일드카드로, $VAR 로 단일 노드를 매치하거나 $$$PARAMS 로 다중 노드 패턴을 매치할 수 있습니다.
StringUtils.isEmpty($ARG) 사용 패턴은?

패턴 재작성(Pattern Rewrite)으로 안전한 리팩터링하기
리팩터링의 영향 범위도 패턴 검색으로 확인할 수 있습니다. 라이브러리에서 메서드 시그니처를 바꾸는 일은 위험합니다. 어디서 호출되는지 빠짐없이 알아야 하기 때문입니다. AST 기반 패턴 검색은 텍스트 검색과 달리 주석이나 문자열에 든 같은 이름을 걸러내고 실제 호출 노드만 잡습니다.
StringUtils.isEmpty($ARG)를 StringUtils.isEmpty($ARG, false)로 패턴 재작성해줘. dryrun 을 먼저 수행해.
패턴 검색으로 범위를 확인하고, dry-run 플래그를 설정하여 바로 적용하지 않고 결과를 검토한 뒤 적용할 수 있습니다. 텍스트 일괄 치환과 달리 구문 구조를 기준으로 하므로, 의도치 않은 부분이 함께 바뀌는 일을 줄입니다. 이 흐름은 코드가 수천 줄이든 십만 줄이든 동일합니다. 달라지는 것은 사람이 직접 했을 때의 비용뿐입니다.
패턴 검색이 찾는 것은 “이 형태로 호출하는 곳”입니다. 하지만 오버로드된 메서드나 상속 계층까지 빠짐없이 추적하려면 타입 정보가 필요합니다. 이때 /code init으로 LSP를 켜면, find references로 타입 기반 참조를 완전히 수집하고, 변경 후 발생하는 타입 오류도 즉시 확인할 수 있습니다.
한 가지 덧붙이면, 이 방식이 토큰을 전혀 쓰지 않는다는 뜻은 아닙니다. 파싱과 심볼 검색 자체는 LLM 없이 수행되지만, /code overview처럼 결과를 정리해 모델의 컨텍스트에 올릴 때는 그 분량만큼 토큰이 듭니다(대형 레포지토리에서는 개요 한 번에 수만 토큰이 들 수 있습니다). 다만 RAG처럼 임베딩 파이프라인과 벡터 DB를 따로 구축·운영하는 비용이 없고, 매번 grep으로 전체를 훑으며 무관한 파일까지 컨텍스트에 쌓는 낭비를 줄인다는 점이 차이입니다.
| 구분 | RAG (벡터) | AST |
|---|---|---|
| 처리 | 코드→청킹→임베딩→유사도 | 코드→파싱→트리→관계 |
| 구조 | 소실 | 보존 |
| 청킹 | 필요 | 불필요(경계 자동 인식) |
| 인프라 | 벡터 DB | 없음 |
그 밖의 활용
| 상황 | 활용 |
|---|---|
| 심볼·참조 탐색 | Symbol search로 클래스·메서드를 찾고, Find references로 사용처를 확인 |
| 새 기능 추가 | Go to definition으로 관련 타입과 호출 지점의 위치를 정확히 파악 |
| 버그 추적 | 정의 이동과 참조 찾기를 이어가며 호출 관계를 따라감 |
| 폴리글랏(Polyglot) 프로젝트 | tree-sitter 18종 언어 지원으로, 프론트엔드 JS/TS가 섞여도 동일한 방식으로 탐색 |
| 정밀 분석 | /code init으로 LSP를 켜고, 변경 후 타입 오류를 즉시 잡거나 심볼의 시그니처를 바로 조회 |
5. 마무리: 모델이 아니라 구조
대규모·레거시 코드베이스에서 AI 코딩의 병목은 모델의 성능이 아니라, 모델에 전달되는 코드 이해의 품질입니다. 텍스트 유사도 기반 검색은 좌표를 줄 뿐 길을 알려주지 못합니다. 코드를 문법 구조(AST)로 읽으면 호출·구현·의존 관계가 보존되고, 모델은 그 위에서 더 정확하게 판단합니다.
Kiro Code Intelligence는 이 접근을 tree-sitter와 LSP 두 레이어로 내장했습니다. tree-sitter로 설정 없이 시작하고, 필요하면 LSP로 정밀도를 더합니다. 별도의 벡터 DB나 임베딩 파이프라인을 구축하지 않고 기존 워크플로 안에서 동작합니다.
더 똑똑한 모델을 기다리기보다, 지금 가진 모델에 코드베이스의 구조를 제대로 전달하는 것이 대규모 코드 작업의 생산성을 좌우합니다.
참고
- Kiro Code Intelligence 공식 문서: https://kiro.dev/docs/cli/code-intelligence/
- tree-sitter: https://tree-sitter.github.io/tree-sitter/
- Spring PetClinic: https://github.com/spring-projects/spring-petclinic (Apache-2.0)
- Apache Commons Lang: https://github.com/apache/commons-lang (Apache-2.0)