Ollama Custom CLI 명령어 시스템 심층 분석

Ollama Custom의 CLI 시스템은 Cobra 프레임워크를 기반으로 구현된 강력하고 직관적인 명령행 인터페이스입니다. 이 포스트에서는 cmd/cmd.go의 1,455라인에 걸친 복잡한 CLI 구현을 상세히 분석해보겠습니다.

1. CLI 아키텍처 개요

1.1 Cobra 프레임워크 활용

func NewCLI() *cobra.Command {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    cobra.EnableCommandSorting = false
    
    // 루트 명령어 정의
    rootCmd := &cobra.Command{
        Use:   "ollama",
        Short: "Large language model runner",
        CompletionOptions: cobra.CompletionOptions{
            DisableDefaultCmd: true,
        },
        Run: DefaultHandler,
    }
    
    // 서브 명령어 등록
    rootCmd.AddCommand(
        serveCmd, createCmd, showCmd, runCmd, stopCmd,
        pullCmd, pushCmd, listCmd, psCmd, copyCmd, deleteCmd,
    )
    
    return rootCmd
}

설계 특징:

  • 계층적 구조: 루트 명령어 아래 기능별 서브 명령어
  • 자동 완성 비활성화: DisableDefaultCmd: true로 커스텀 제어
  • 명령어 정렬 제어: 사용자 정의 순서로 명령어 표시

1.2 핵심 명령어 구조

ollama
├── serve          # 서버 실행
├── create         # 모델 생성
├── show           # 모델 정보 표시
├── run            # 모델 실행 및 대화
├── stop           # 모델 중지
├── pull           # 모델 다운로드
├── push           # 모델 업로드
├── list           # 모델 목록
├── ps             # 실행 중인 모델 목록
├── copy           # 모델 복사
└── delete         # 모델 삭제

2. 주요 핸들러 함수 분석

2.1 CreateHandler - 모델 생성

func CreateHandler(cmd *cobra.Command, args []string) error {
    p := progress.NewProgress(os.Stderr)
    defer p.Stop()
    
    var reader io.Reader
    filename, err := getModelfileName(cmd)
    if os.IsNotExist(err) {
        if filename == "" {
            reader = strings.NewReader("FROM .\n")  // 기본 Modelfile
        } else {
            return errModelfileNotFound
        }
    }
    
    // Modelfile 파싱
    modelfile, err := parser.ParseFile(reader)
    if err != nil {
        return err
    }
    
    // 비동기 blob 생성
    var g errgroup.Group
    g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1))
    
    files := syncmap.NewSyncMap[string, string]()
    for f, digest := range req.Files {
        g.Go(func() error {
            if _, err := createBlob(cmd, client, f, digest, p); err != nil {
                return err
            }
            files.Store(filepath.Base(f), digest)
            return nil
        })
    }
    
    return g.Wait()
}

핵심 기능:

  • Modelfile 처리: 사용자 정의 모델 설정 파일 파싱
  • 병렬 처리: errgroup을 활용한 동시 파일 업로드
  • 진행률 표시: 실시간 업로드 진행률 모니터링
  • 에러 복구: 각 단계별 상세한 에러 처리

2.2 RunHandler - 대화형 모델 실행

func RunHandler(cmd *cobra.Command, args []string) error {
    interactive := true
    
    opts := runOptions{
        Model:    args[0],
        WordWrap: os.Getenv("TERM") == "xterm-256color",
        Options:  map[string]any{},
    }
    
    // 명령행 플래그 처리
    format, err := cmd.Flags().GetString("format")
    if err != nil {
        return err
    }
    opts.Format = format
    
    // 비대화형 모드 처리
    if msg, _ := cmd.Flags().GetString("message"); msg != "" {
        interactive = false
        opts.Prompt = msg
    }
    
    if interactive {
        return generateInteractive(cmd, opts)
    }
    
    return generate(cmd, opts)
}

상호작용 모드:

  • 대화형 모드: 실시간 사용자 입력 처리
  • 배치 모드: 단일 프롬프트 실행 후 종료
  • 터미널 감지: 환경에 따른 워드 랩핑 최적화
  • 플래그 통합: 다양한 실행 옵션 지원

2.3 진행률 추적 시스템

func createBlob(cmd *cobra.Command, client *api.Client, path string, digest string, p *progress.Progress) (string, error) {
    // 파일 정보 획득
    fileInfo, err := bin.Stat()
    if err != nil {
        return "", err
    }
    fileSize := fileInfo.Size()
    
    var pw progressWriter
    status := fmt.Sprintf("copying file %s 0%%", digest)
    spinner := progress.NewSpinner(status)
    p.Add(status, spinner)
    
    // 비동기 진행률 업데이트
    done := make(chan struct{})
    go func() {
        ticker := time.NewTicker(60 * time.Millisecond)
        defer ticker.Stop()
        for {
            select {
            case <-ticker.C:
                percentage := int(100 * pw.n.Load() / fileSize)
                spinner.SetMessage(fmt.Sprintf("copying file %s %d%%", digest, percentage))
            case <-done:
                spinner.SetMessage(fmt.Sprintf("copying file %s 100%%", digest))
                return
            }
        }
    }()
    
    return client.CreateBlob(cmd.Context(), digest, io.TeeReader(bin, &pw))
}

진행률 시스템:

  • 실시간 업데이트: 60ms 간격으로 진행률 갱신
  • 원자적 카운터: atomic.Int64로 동시성 안전성 보장
  • TeeReader 활용: 데이터 전송과 동시에 진행률 추적
  • 채널 동기화: Goroutine 간 안전한 통신

3. 환경 설정 및 구성 관리

3.1 환경 변수 문서화

func appendEnvDocs(cmd *cobra.Command, envs []envconfig.EnvVar) {
    if len(envs) == 0 {
        return
    }
    
    envUsage := `
Environment Variables:
`
    for _, e := range envs {
        envUsage += fmt.Sprintf("      %-24s   %s\n", e.Name, e.Description)
    }
    
    cmd.SetUsageTemplate(cmd.UsageTemplate() + envUsage)
}

3.2 명령어별 환경 변수 매핑

// 서버 명령어에 특화된 환경 변수들
case serveCmd:
    appendEnvDocs(cmd, []envconfig.EnvVar{
        envVars["OLLAMA_DEBUG"],           // 디버그 모드
        envVars["OLLAMA_HOST"],            // 서버 호스트
        envVars["OLLAMA_KEEP_ALIVE"],      // 모델 유지 시간
        envVars["OLLAMA_MAX_LOADED_MODELS"], // 최대 로드 모델 수
        envVars["OLLAMA_MAX_QUEUE"],       // 최대 큐 크기
        envVars["OLLAMA_MODELS"],          // 모델 저장 경로
        envVars["OLLAMA_NUM_PARALLEL"],    // 병렬 처리 수
        envVars["OLLAMA_FLASH_ATTENTION"], // Flash Attention 사용
        envVars["OLLAMA_KV_CACHE_TYPE"],   // KV 캐시 타입
        envVars["OLLAMA_GPU_OVERHEAD"],    // GPU 오버헤드
    })

구성 관리 특징:

  • 명령어별 특화: 각 명령어에 필요한 환경 변수만 표시
  • 자동 문서화: 도움말에 환경 변수 설명 자동 추가
  • 타입 안전성: envconfig.EnvVar 구조체로 타입 보장

4. 에러 처리 및 복구 전략

4.1 계층화된 에러 처리

func StopHandler(cmd *cobra.Command, args []string) error {
    opts := &runOptions{
        Model:     args[0],
        KeepAlive: &api.Duration{Duration: 0},
    }
    
    if err := loadOrUnloadModel(cmd, opts); err != nil {
        if strings.Contains(err.Error(), "not found") {
            return fmt.Errorf("couldn't find model \"%s\" to stop", args[0])
        }
        return err
    }
    return nil
}

에러 처리 패턴:

  • 에러 분류: 에러 메시지 내용에 따른 적절한 대응
  • 사용자 친화적 메시지: 기술적 에러를 이해하기 쉬운 메시지로 변환
  • 문맥 보존: 원본 에러 정보 유지하면서 추가 정보 제공

4.2 버전 호환성 체크

func versionHandler(cmd *cobra.Command, _ []string) {
    client, err := api.ClientFromEnvironment()
    if err != nil {
        return
    }
    
    serverVersion, err := client.Version(cmd.Context())
    if err != nil {
        fmt.Println("Warning: could not connect to a running Ollama instance")
    }
    
    if serverVersion != "" {
        fmt.Printf("ollama version is %s\n", serverVersion)
    }
    
    // 클라이언트-서버 버전 불일치 경고
    if serverVersion != version.Version {
        fmt.Printf("Warning: client version is %s\n", version.Version)
    }
}

5. 성능 최적화 기법

5.1 동시성 제어

// CPU 코어 수에 기반한 goroutine 제한
var g errgroup.Group
g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1))

// 동시성 안전 맵 활용
files := syncmap.NewSyncMap[string, string]()
adapters := syncmap.NewSyncMap[string, string]()

for f, digest := range req.Files {
    g.Go(func() error {
        if _, err := createBlob(cmd, client, f, digest, p); err != nil {
            return err
        }
        files.Store(filepath.Base(f), digest)
        return nil
    })
}

최적화 전략:

  • 적응형 동시성: 시스템 리소스에 따른 동적 제한
  • 메모리 안전성: 타입 안전 동시성 맵 사용
  • 리소스 관리: 고루틴 누수 방지를 위한 제한 설정

5.2 스트리밍 처리

type progressWriter struct {
    n atomic.Int64
}

func (w *progressWriter) Write(p []byte) (n int, err error) {
    w.n.Add(int64(len(p)))
    return len(p), nil
}

// TeeReader를 활용한 스트리밍 업로드
err := client.CreateBlob(cmd.Context(), digest, io.TeeReader(bin, &pw))

스트리밍 장점:

  • 메모리 효율성: 대용량 파일을 청크 단위로 처리
  • 진행률 추적: 실시간 업로드 진행률 모니터링
  • 원자적 업데이트: Lock-free 카운터로 성능 향상

6. 사용자 경험 최적화

6.1 대화형 인터페이스

func generateInteractive(cmd *cobra.Command, opts runOptions) error {
    // 터미널 설정
    if opts.WordWrap {
        scanner := bufio.NewScanner(os.Stdin)
        for {
            fmt.Print(">>> ")
            if !scanner.Scan() {
                break
            }
            
            prompt := scanner.Text()
            if prompt == "/bye" {
                break
            }
            
            // 실시간 응답 스트리밍
            err := streamResponse(cmd, opts, prompt)
            if err != nil {
                fmt.Printf("Error: %v\n", err)
            }
        }
    }
    return nil
}

UX 개선사항:

  • 직관적 프롬프트: >>> 표시로 입력 대기 상태 명확화
  • 종료 명령어: /bye로 자연스러운 대화 종료
  • 실시간 피드백: 스트리밍 응답으로 즉각적인 반응

6.2 진행률 시각화

func (resp api.ProgressResponse) error {
    if resp.Digest != "" {
        bar, ok := bars[resp.Digest]
        if !ok {
            msg := resp.Status
            if msg == "" {
                msg = fmt.Sprintf("pulling %s...", resp.Digest[7:19])
            }
            bar = progress.NewBar(msg, resp.Total, resp.Completed)
            bars[resp.Digest] = bar
            p.Add(resp.Digest, bar)
        }
        bar.Set(resp.Completed)
    }
    return nil
}

7. 확장성 및 유지보수성

7.1 명령어 추가 패턴

// 새로운 명령어 등록 패턴
newCmd := &cobra.Command{
    Use:   "newcommand",
    Short: "Description of new command",
    Args:  cobra.ExactArgs(1),
    RunE:  NewCommandHandler,
}

// 플래그 추가
newCmd.Flags().StringP("option", "o", "", "Option description")

// 루트에 추가
rootCmd.AddCommand(newCmd)

7.2 에러 타입 확장

var (
    errModelfileNotFound = errors.New("specified Modelfile wasn't found")
    errInvalidFormat     = errors.New("invalid format specified")
    errConnectionFailed  = errors.New("failed to connect to server")
)

8. 결론

Ollama Custom의 CLI 시스템은 현대적인 Go 개발 패턴을 충실히 따르는 설계를 보여줍니다:

8.1 주요 장점

  • 모듈화: 각 명령어가 독립적인 핸들러로 분리
  • 동시성: 효율적인 비동기 처리와 진행률 추적
  • 확장성: 새로운 명령어 추가가 용이한 구조
  • 사용자 중심: 직관적인 인터페이스와 상세한 피드백

8.2 성능 특징

  • 리소스 효율성: CPU 코어 수에 적응하는 동시성 제어
  • 메모리 최적화: 스트리밍 처리로 대용량 파일 처리
  • 응답성: 실시간 진행률 표시로 사용자 경험 향상

다음 포스트에서는 서버 아키텍처와 HTTP API 라우팅 시스템을 분석해보겠습니다.