서론
오픈소스 기여모임 9기가 끝이 났습니다.
저는 기여모임 내에서 다양한 오픈소스에 PR을 생성했습니다.
- nest: 6개의 PR
- loki: 1개의 PR
- prisma: 1개의 PR(Merged)
- gemini-cli: 1개의 PR(Merged)
이 중, gemini-cli는 현재 AI를 다루는 능력이 거의 필수 스택으로 자리잡았고, 저에게 가장 친숙한 TypeScript기반이라는 점, 마지막으로 내가 구글에 기여할 수 있다니!!! 와 같은 이유로 기여하기로 했는데요 ㅋㅋ..
그래서, 이번 포스팅은 gemini-cli의 기여에 대한 포스팅입니다.
Gemini-CLI
현 시점 CLI 기반 AI의 양대산맥이라고 한다면, claude code와 gemini-cli가 대표적인데요.
여러 차이가 있겠지만, 오픈소스 성격을 띠는지? 의 차이도 있는 것 같아요.
특히, gemini-cli의 경우 공개적인 로드맵까지 작성되어있어, 관심 있는 이슈를 직접 기여해볼 수 있도록 기여자들의 참여를 적극 장려하고 있는 상황입니다.
저는 오늘, 9개월 전 첫 오픈소스 기여를 시작하면서 막연하게 꿈꿔왔던 목표인
직접 이슈를 발견해서 등록하고, 해결해보기
를 달성하고, 더불어 모든 사용자에게 영향을 줄 수 있었던 이슈를 발견하고,
과정에서 AI를 활용하며 기여했던 과정 전반의 경험을 공유드리려고 합니다.
어떻게 이슈를 발견할까?
오픈소스의 코드는 방대합니다. 주당 몇 만자씩 추가되는 이 방대한 오픈소스에서, 어떻게 이슈를 발견해야할까요? 마냥 하나하나 파일을 분석하기에는, 너무 비효율적입니다.
저는 효과적으로 분석하기 위해 우선 UX의 흐름을 생각해보기로 했습니다.
- gemini-cli설치하기
- 터미널에 "gemini" 명령어 실행하기
- 질문하고 응답받고의 반복
방대한 코드를, UX의 흐름으로 재정의하고보니 엄청 단순해졌습니다.
저는 이 흐름에서 명령어를 실행한 후 interactive mode(대화형 모드)가 생성되기 전까지의 동작을 확인해보기로 결정했습니다.
즉, 초기화 단계를 중점으로 파헤쳐보기로 한 것이죠.
AI와 함께 분석하기
잘 짜여진 코드 덕분인지 초기화 단계는 금방 찾을 수 있었습니다. 하지만 가시성이 아무리 좋고 좋은 구조로 짜여져 있다고 하더라도, 처음 보는 프로젝트의 모든 상호작용을 파악하면서 정확히 어떤 과정들을 수행하는지 한 눈에 알기는 어려웠습니다.
이 때, 시간 낭비를 줄이기 위해 저는 AI를 활용했습니다.
처음에는 할루시네이션 때문에 오픈소스에서 AI를 활용하는 것은 바람직한가? 라는 의문을 품었습니다.
하지만 부정할 수 없는 사실은 AI는 이미, 어쩌면 처음 등장했을 때 부터 제가 알고있는 프로그래밍 지식보다 훨씬 많은 것을 알고 있다는 것을요. 그리고 AI 활용법에 관한 많은 레퍼런스들에서 공통으로 얘기하듯, AI에게 판단을 맡기지 않고 사실 기반으로만 동작하게 한다면 AI는 최고의 동료가 될 것이라는 것을요.
저는 그래서, 짜여진 코드라는 사실에 기반하여 초기화 과정 자체를 분석하는 일을 AI에게 맡겼습니다.
DeepWiki: AI 기반 깃허브 저장소 위키
DeepWiki는 Devin AI를 만든 Cognition에서 만든 위키 형태의 AI 입니다.
깃허브 저장소를 위키 형태로 변환하여 볼 수 있게 해주는 도구로, 오픈소스의 핵심 아키텍처나 오픈소스 내의 피처들이 어떤 흐름으로 동작하는지 등 오픈소스의 전반적인 것들을 분석해주는 도구입니다.
위 사진은, 애플리케이션의 아키텍처를 DeepWiki가 분석해준 워크플로우이고, 빨간 네모 박스는 실행 시 초기화 과정 전반의 동작들입니다. 이를 기반으로 각 함수들을 하나씩 분석해 본 결과, 단순히 사용자의 세팅을 불러오는 구간들을 배제할 수 있었습니다.
작업할 구간을 좁히고 좁히다보니, memoryDiscovery라는 파일에서 퍼포먼스 개선이 가능해보이는 코드를 발견할 수 있었습니다.
memoryDiscovery는 초기화 시 GEMINI-CLI가 호출된 디렉토리를 기준으로, 상/하향 디렉토리들을 순차적으로 확인하여 GEMINI.md가 있는 경로를 수집합니다. 수집이 완료되면, 수집된 경로에서 순차적으로 마크다운 파일을 처리하여 메모리에 올려 사용합니다.
이 과정을 간단하게 도식화해본다면, 아래와 같은 형태입니다.
모든 디렉토리/파일에 대해 순차적인 처리를 수행하기 때문에, 프로젝트가 커지면 문제가 발생하겠다고 생각했고, 이 구간에 대해 작업을 시작했습니다.
Claude Code: 교차 검증
현재 AI는 아시다시피 할루시네이션이 엄청 심합니다.
DeepWiki를 통해 사실 기반의 워크플로우를 추론할 수 있었지만, 사실 확인이 한 번 더 필요하다고 판단했습니다.
이를 위해, GEMINI-CLI를 로컬에 클론시킨 뒤, CLAUDE CODE를 통해서 해당 작업 구간을 다시 한 번 재확인했습니다.
PR 생성
작업 구간이 명확해졌으니, 코드 작업을 해야겠죠?
제가 작업한 최종 코드는 다음과 같습니다.
- 기존 순차처리를 병렬로 변경
- EMFILE 에러 방지를 위한 동시성 제한 추가
- Promise.allSettled 사용으로 문제 발생 시에도 안정적인 파일 처리
PR을 생성하는 과정에서, Promise.allSettled는 gemini가 직접 제안해준 리뷰 내용에 포함되어 있어 추가하였습니다.
(뭔가 제가 만든 요리를 레시피 원작자가 직접 평가하는 기분이네요)
결과
생각보다 금방 머지가되어, GEMINI-CLI의 컨트리뷰터가 될 수 있었습니다.
PR 과정에서 얼마나 개선될까 싶어 벤치마크 테스트를 PR의 커밋에 추가했었는데요,
아래 코드에서 알 수 있듯이, 큰 규모의 테스트는 아니지만 약 60% 가량의 퍼포먼스 향상을 이루어낼 수 있었습니다.
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, beforeEach, afterEach, expect } from 'vitest';
import * as fs from 'fs/promises';
import * as path from 'path';
import { tmpdir } from 'os';
import { loadServerHierarchicalMemory } from './memoryDiscovery.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { processImports } from './memoryImportProcessor.js';
// Helper to create test content
function createTestContent(index: number): string {
return `# GEMINI Configuration ${index}
## Project Instructions
This is test content for performance benchmarking.
The content should be substantial enough to simulate real-world usage.
### Code Style Guidelines
- Use TypeScript for type safety
- Follow functional programming patterns
- Maintain high test coverage
- Keep functions pure when possible
### Architecture Principles
- Modular design with clear boundaries
- Clean separation of concerns
- Efficient resource usage
- Scalable and maintainable codebase
### Development Guidelines
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
`.repeat(3); // Make content substantial
}
// Sequential implementation for comparison
async function readFilesSequential(
filePaths: string[],
): Promise<Array<{ path: string; content: string | null }>> {
const results = [];
for (const filePath of filePaths) {
try {
const content = await fs.readFile(filePath, 'utf-8');
const processedResult = await processImports(
content,
path.dirname(filePath),
false,
undefined,
undefined,
'flat',
);
results.push({ path: filePath, content: processedResult.content });
} catch {
results.push({ path: filePath, content: null });
}
}
return results;
}
// Parallel implementation
async function readFilesParallel(
filePaths: string[],
): Promise<Array<{ path: string; content: string | null }>> {
const promises = filePaths.map(async (filePath) => {
try {
const content = await fs.readFile(filePath, 'utf-8');
const processedResult = await processImports(
content,
path.dirname(filePath),
false,
undefined,
undefined,
'flat',
);
return { path: filePath, content: processedResult.content };
} catch {
return { path: filePath, content: null };
}
});
return Promise.all(promises);
}
describe('memoryDiscovery performance', () => {
let testDir: string;
let fileService: FileDiscoveryService;
beforeEach(async () => {
testDir = path.join(tmpdir(), `memoryDiscovery-perf-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
fileService = new FileDiscoveryService(testDir);
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('should demonstrate significant performance improvement with parallel processing', async () => {
// Create test structure
const numFiles = 20;
const filePaths: string[] = [];
for (let i = 0; i < numFiles; i++) {
const dirPath = path.join(testDir, `project-${i}`);
await fs.mkdir(dirPath, { recursive: true });
const filePath = path.join(dirPath, 'GEMINI.md');
await fs.writeFile(filePath, createTestContent(i));
filePaths.push(filePath);
}
// Measure sequential processing
const seqStart = performance.now();
const seqResults = await readFilesSequential(filePaths);
const seqTime = performance.now() - seqStart;
// Measure parallel processing
const parStart = performance.now();
const parResults = await readFilesParallel(filePaths);
const parTime = performance.now() - parStart;
// Verify results are equivalent
expect(seqResults.length).toBe(parResults.length);
expect(seqResults.length).toBe(numFiles);
// Verify parallel is faster
expect(parTime).toBeLessThan(seqTime);
// Calculate improvement
const improvement = ((seqTime - parTime) / seqTime) * 100;
const speedup = seqTime / parTime;
// Log results for visibility
console.log(`\n Performance Results (${numFiles} files):`);
console.log(` Sequential: ${seqTime.toFixed(2)}ms`);
console.log(` Parallel: ${parTime.toFixed(2)}ms`);
console.log(` Improvement: ${improvement.toFixed(1)}%`);
console.log(` Speedup: ${speedup.toFixed(2)}x\n`);
// Expect significant improvement
expect(improvement).toBeGreaterThan(50); // At least 50% improvement
});
it('should handle the actual loadServerHierarchicalMemory function efficiently', async () => {
// Create multiple directories with GEMINI.md files
const dirs: string[] = [];
const numDirs = 10;
for (let i = 0; i < numDirs; i++) {
const dirPath = path.join(testDir, `workspace-${i}`);
await fs.mkdir(dirPath, { recursive: true });
dirs.push(dirPath);
// Create GEMINI.md file
const content = createTestContent(i);
await fs.writeFile(path.join(dirPath, 'GEMINI.md'), content);
// Create nested structure
const nestedPath = path.join(dirPath, 'src', 'components');
await fs.mkdir(nestedPath, { recursive: true });
await fs.writeFile(path.join(nestedPath, 'GEMINI.md'), content);
}
// Measure performance
const startTime = performance.now();
const result = await loadServerHierarchicalMemory(
dirs[0],
dirs.slice(1),
false, // debugMode
fileService,
[], // extensionContextFilePaths
'flat', // importFormat
undefined, // fileFilteringOptions
200, // maxDirs
);
const duration = performance.now() - startTime;
// Verify results
expect(result.fileCount).toBeGreaterThan(0);
expect(result.memoryContent).toBeTruthy();
// Log performance
console.log(`\n Real-world Performance:`);
console.log(
` Processed ${result.fileCount} files in ${duration.toFixed(2)}ms`,
);
console.log(
` Rate: ${(result.fileCount / (duration / 1000)).toFixed(2)} files/second\n`,
);
// Performance should be reasonable
expect(duration).toBeLessThan(1000); // Should complete within 1 second
});
});
마무리
최근 오픈소스 기여를 위해 DeepWiki를 적극 활용하고 있는데요,
위에서도 잠깐 말씀드렸듯이, 사실 기반(=코드베이스 자체만 분석)으로 AI를 활용한다면 엄청난 퍼포먼스를 보이는 것 같습니다.
오픈소스 기여에는 항상 이슈를 해결하기 위한 분석에서 가장 많은 시간을 잡아먹었었는데요, 작업을 위한 분석 구간을 명확하게 좁혀주는 용도로만 사용했지만 거의 PR 생성 속도가 10배 가까이 단축된 것 같아요.
이번 기여에서는, 특히 전 세계 수많은 사용자들의 시간을 매일 조금씩 아껴주었다는 생각에 매우 뿌듯한 경험이었습니다.
모든 gemini-cli의 사용자들이 기존 대비 60%이상 향상된 퍼포먼스를 제 코드를 통해 제공받을 수 있다는 점이 정말 황홀한 것 같아요.
읽어주셔서 감사합니다!