Go로 CLI 앱 만들기: Cobra와 Viper 사용

Go에서 Cobra와 Viper 프레임워크를 사용한 CLI 개발

Page content

명령줄 인터페이스(CLI) 애플리케이션은 개발자, 시스템 관리자, DevOps 전문가에게 필수적인 도구입니다. Go 언어로 CLI 개발을 수행하는 데 사용되는 두 가지 라이브러리인 Cobra(명령 구조)와 Viper(구성 관리)가 표준이 되었습니다.

Go는 CLI 도구를 구축하는 데 탁월한 언어로 자리 잡았습니다. 성능, 간단한 배포, 크로스 플랫폼 지원 덕분입니다.

테트리스

CLI 애플리케이션을 위해 Go를 선택하는 이유

Go는 CLI 개발에 다음과 같은 강점을 제공합니다:

  • 단일 바이너리 배포: 런타임 의존성이나 패키지 관리자 필요 없음
  • 빠른 실행: 네이티브 컴파일로 우수한 성능 제공
  • 크로스 플랫폼 지원: Linux, macOS, Windows 등에서 쉽게 컴파일 가능
  • 강력한 표준 라이브러리: 파일 I/O, 네트워킹, 텍스트 처리에 대한 풍부한 도구 제공
  • 동시성: 병렬 작업을 위한 내장된 goroutines
  • 정적 타이핑: 컴파일 시 오류 검출 가능

Go로 구축된 인기 있는 CLI 도구에는 Docker, Kubernetes(kubectl), Hugo, Terraform, GitHub CLI 등이 있습니다. Go에 새로 시작하거나 빠른 참고가 필요하다면, 필수적인 Go 문법과 패턴을 확인하기 위해 Go Cheat Sheet을 참고하세요.

Cobra 소개

Cobra는 강력한 현대 CLI 애플리케이션을 생성하기 위한 간단한 인터페이스를 제공하는 라이브러리입니다. Hugo와 Viper의 창작자인 Steve Francia(spf13)가 만든 이 라이브러리는 많은 인기 있는 Go 프로젝트에서 사용되고 있습니다.

Cobra의 주요 기능

명령 구조: Cobra는 명령 패턴을 구현하여 git commit 또는 docker run과 같은 명령과 하위 명령을 가진 애플리케이션을 생성할 수 있습니다.

플래그 처리: 자동 파싱과 타입 변환을 지원하는 로컬 및 지속 가능한 플래그.

자동 도움말: 도움말 텍스트와 사용법 정보를 자동으로 생성합니다.

지능형 제안: 사용자가 오타를 입력했을 때 제안을 제공합니다(“Did you mean ‘status’?”).

쉘 완성: bash, zsh, fish, PowerShell을 위한 완성 스크립트 생성.

유연한 출력: 커스텀 포맷터와 출력 스타일과 원활하게 작동.

Viper 소개

Viper는 Go 애플리케이션을 위한 완전한 구성 솔루션으로, Cobra와 원활하게 작동하도록 설계되었습니다. 여러 출처에서의 구성과 명확한 우선순위 순서를 처리합니다.

Viper의 주요 기능

다중 구성 출처:

  • 구성 파일 (JSON, YAML, TOML, HCL, INI, envfile, Java properties)
  • 환경 변수
  • 명령줄 플래그
  • 원격 구성 시스템 (etcd, Consul)
  • 기본값

구성 우선순위 순서:

  1. 명시적인 Set 호출
  2. 명령줄 플래그
  3. 환경 변수
  4. 구성 파일
  5. 키/값 저장소
  6. 기본값

라이브 모니터링: 구성 파일을 모니터링하고 변경 시 자동으로 다시 로드합니다.

타입 변환: 다양한 Go 타입 (문자열, 정수, 불리언, 기간 등)으로 자동 변환.

시작하기: 설치

먼저 새로운 Go 모듈을 초기화하고 두 라이브러리를 설치합니다:

go mod init myapp
go get -u github.com/spf13/cobra@latest
go get -u github.com/spf13/viper@latest

선택적으로, 스프래프팅을 위한 Cobra CLI 생성기를 설치할 수 있습니다:

go install github.com/spf13/cobra-cli@latest

첫 번째 CLI 애플리케이션 구축

실용적인 예제를 만들어 보겠습니다: 구성 지원이 있는 작업 관리 CLI 도구.

프로젝트 구조

mytasks/
├── cmd/
│   ├── root.go
│   ├── add.go
│   ├── list.go
│   └── complete.go
├── config/
│   └── config.go
├── main.go
└── config.yaml

메인 진입점

// main.go
package main

import "mytasks/cmd"

func main() {
    cmd.Execute()
}

Viper 통합을 위한 루트 명령

// cmd/root.go
package cmd

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   "mytasks",
    Short: "간단한 작업 관리 CLI",
    Long: `MyTasks는 쉽게 작업을 정리할 수 있는 CLI 작업 관리자입니다. 
Cobra와 Viper로 구축되었습니다.`,
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func init() {
    cobra.OnInitialize(initConfig)
    
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", 
        "구성 파일 (기본값은 $HOME/.mytasks.yaml)")
    rootCmd.PersistentFlags().String("db", "", 
        "데이터베이스 파일 위치")
    
    viper.BindPFlag("database", rootCmd.PersistentFlags().Lookup("db"))
}

func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        
        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".mytasks")
    }
    
    viper.SetEnvPrefix("MYTASKS")
    viper.AutomaticEnv()
    
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("사용 중인 구성 파일:", viper.ConfigFileUsed())
    }
}

하위 명령 추가

// cmd/add.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var priority string

var addCmd = &cobra.Command{
    Use:   "add [작업 설명]",
    Short: "새로운 작업 추가",
    Args:  cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        task := args[0]
        db := viper.GetString("database")
        
        fmt.Printf("작업 추가: %s\n", task)
        fmt.Printf("우선순위: %s\n", priority)
        fmt.Printf("데이터베이스: %s\n", db)
        
        // 여기서 실제 작업 저장을 구현해야 합니다
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
    
    addCmd.Flags().StringVarP(&priority, "priority", "p", "medium", 
        "작업 우선순위 (low, medium, high)")
}

목록 명령

// cmd/list.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var showCompleted bool

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "모든 작업 목록 보기",
    Run: func(cmd *cobra.Command, args []string) {
        db := viper.GetString("database")
        
        fmt.Printf("작업 목록 가져오기: %s\n", db)
        fmt.Printf("완료된 작업 표시: %v\n", showCompleted)
        
        // 여기서 실제 작업 목록을 구현해야 합니다
    },
}

func init() {
    rootCmd.AddCommand(listCmd)
    
    listCmd.Flags().BoolVarP(&showCompleted, "completed", "c", false, 
        "완료된 작업 표시")
}

데이터 지속성 구현

생산용 작업 관리 CLI를 구축하려면 실제 데이터 저장을 구현해야 합니다. 여기서는 간단한 데이터베이스 경로 구성만 사용하고 있지만, 데이터를 지속적으로 저장하기 위해 여러 옵션이 있습니다:

  • SQLite: CLI 도구에 적합한 가볍고 서버 없는 데이터베이스
  • PostgreSQL/MySQL: 더 복잡한 애플리케이션에 적합한 완전한 기능의 데이터베이스
  • JSON/YAML 파일: 가벼운 요구사항에 적합한 파일 기반 저장
  • 내장 데이터베이스: BoltDB, BadgerDB와 같은 키-값 저장소

PostgreSQL과 같은 관계형 데이터베이스를 사용하는 경우 ORM 또는 쿼리 빌더를 사용해야 합니다. Go 데이터베이스 라이브러리에 대한 종합적인 비교를 원하시면, PostgreSQL을 위한 Go ORMs 비교: GORM vs Ent vs Bun vs sqlc 가이드를 참고하세요.

Viper를 사용한 고급 구성

구성 파일 예제

# .mytasks.yaml
database: ~/.mytasks.db
format: table
colors: true

notifications:
  enabled: true
  sound: true

priorities:
  high: red
  medium: yellow
  low: green

중첩 구성 읽기

func getNotificationSettings() {
    enabled := viper.GetBool("notifications.enabled")
    sound := viper.GetBool("notifications.sound")
    
    fmt.Printf("알림 활성화: %v\n", enabled)
    fmt.Printf("음성 활성화: %v\n", sound)
}

func getPriorityColors() map[string]string {
    return viper.GetStringMapString("priorities")
}

환경 변수

Viper는 설정된 접두사와 함께 환경 변수를 자동으로 읽습니다:

export MYTASKS_DATABASE=/tmp/tasks.db
export MYTASKS_NOTIFICATIONS_ENABLED=false
mytasks list

라이브 구성 재로드

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        fmt.Println("구성 파일 변경:", e.Name)
        // 구성에 의존하는 구성 요소 재로드
    })
}

최고의 실천 방법

1. 일관성을 위해 Cobra 생성기 사용

cobra-cli init
cobra-cli add serve
cobra-cli add create

2. 명령을 별도 파일에 정리

cmd/ 디렉토리 아래에 각 명령을 별도의 파일에 두어 유지보수성을 높이세요.

3. 지속 가능한 플래그 사용

모든 하위 명령에 적용되는 옵션을 위해 지속 가능한 플래그를 사용하세요:

rootCmd.PersistentFlags().StringP("output", "o", "text", 
    "출력 형식 (text, json, yaml)")

4. 적절한 오류 처리 구현

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "내 애플리케이션",
    RunE: func(cmd *cobra.Command, args []string) error {
        if err := doSomething(); err != nil {
            return fmt.Errorf("실행 실패: %w", err)
        }
        return nil
    },
}

5. 의미 있는 도움말 텍스트 제공

var cmd = &cobra.Command{
    Use:   "deploy [환경]",
    Short: "지정된 환경으로 애플리케이션 배포",
    Long: `빌드 및 지정된 환경으로 애플리케이션을 배포합니다. 지원되는 환경은 다음과 같습니다:
  - 개발 (dev)
  - 스테이징
  - 프로덕션 (prod)`,
    Example: `  myapp deploy staging
  myapp deploy production --version=1.2.3`,
}

6. 합리적인 기본값 설정

func init() {
    viper.SetDefault("port", 8080)
    viper.SetDefault("timeout", "30s")
    viper.SetDefault("retries", 3)
}

7. 구성 검증

func validateConfig() error {
    port := viper.GetInt("port")
    if port < 1024 || port > 65535 {
        return fmt.Errorf("잘못된 포트: %d", port)
    }
    return nil
}

CLI 애플리케이션 테스트

명령 테스트

// cmd/root_test.go
package cmd

import (
    "bytes"
    "testing"
)

func TestRootCommand(t *testing.T) {
    cmd := rootCmd
    b := bytes.NewBufferString("")
    cmd.SetOut(b)
    cmd.SetArgs([]string{"--help"})
    
    if err := cmd.Execute(); err != nil {
        t.Fatalf("명령 실행 실패: %v", err)
    }
}

다양한 구성으로 테스트

func TestWithConfig(t *testing.T) {
    viper.Set("database", "/tmp/test.db")
    viper.Set("debug", true)
    
    // 테스트 실행
    
    viper.Reset() // 정리
}

쉘 완성 생성

Cobra는 다양한 쉘용 완성 스크립트를 생성할 수 있습니다:

// cmd/completion.go
package cmd

import (
    "os"
    "github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "완성 스크립트 생성",
    Long: `완성 로드 방법:

Bash:
  $ source <(mytasks completion bash)

Zsh:
  $ source <(mytasks completion zsh)

Fish:
  $ mytasks completion fish | source

PowerShell:
  PS> mytasks completion powershell | Out-String | Invoke-Expression
`,
    DisableFlagsInUseLine: true,
    ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
    Args: cobra.ExactValidArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        switch args[0] {
        case "bash":
            cmd.Root().GenBashCompletion(os.Stdout)
        case "zsh":
            cmd.Root().GenZshCompletion(os.Stdout)
        case "fish":
            cmd.Root().GenFishCompletion(os.Stdout, true)
        case "powershell":
            cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
        }
    },
}

func init() {
    rootCmd.AddCommand(completionCmd)
}

빌드 및 배포

여러 플랫폼을 위한 빌드

# Linux
GOOS=linux GOARCH=amd64 go build -o mytasks-linux-amd64

# macOS
GOOS=darwin GOARCH=amd64 go build -o mytasks-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o mytasks-darwin-arm64

# Windows
GOOS=windows GOARCH=amd64 go build -o mytasks-windows-amd64.exe

바이너리 크기 축소

go build -ldflags="-s -w" -o mytasks

버전 정보 추가

// cmd/version.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var (
    Version   = "dev"
    Commit    = "none"
    BuildTime = "unknown"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "버전 정보 출력",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("버전: %s\n", Version)
        fmt.Printf("커밋: %s\n", Commit)
        fmt.Printf("빌드 시간: %s\n", BuildTime)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}

버전 정보와 함께 빌드:

go build -ldflags="-X 'mytasks/cmd.Version=1.0.0' \
    -X 'mytasks/cmd.Commit=$(git rev-parse HEAD)' \
    -X 'mytasks/cmd.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" \
    -o mytasks

실제 사례

Cobra와 Viper로 구축된 인기 있는 오픈소스 도구:

  • kubectl: Kubernetes 명령줄 도구
  • Hugo: 정적 사이트 생성기
  • GitHub CLI (gh): GitHub 공식 CLI
  • Docker CLI: 컨테이너 관리
  • Helm: Kubernetes 패키지 관리자
  • Skaffold: Kubernetes 개발 워크플로우 도구
  • Cobra CLI: 자체 호스팅 - Cobra는 자신을 사용합니다!

전통적인 DevOps 도구를 넘어, Go의 CLI 기능은 AI 및 머신러닝 애플리케이션으로도 확장됩니다. 대규모 언어 모델과 상호작용하는 CLI 도구를 구축하려면, Ollama와 Go를 사용한 구조화된 출력으로 LLM 제한에 대한 우리의 가이드를 참고하세요. 이 가이드는 AI 모델과 함께 작동하는 Go 애플리케이션을 어떻게 구축할 수 있는지 보여줍니다.

유용한 자료

결론

Cobra와 Viper는 Go에서 전문적인 CLI 애플리케이션을 구축하기 위한 강력한 기반을 제공합니다. Cobra는 명령 구조, 플래그 파싱, 도움말 생성을 처리하고, Viper는 여러 출처에서의 구성과 지능적인 우선순위를 관리합니다.

이 조합은 다음과 같은 CLI 도구를 생성할 수 있도록 합니다:

  • 직관적인 명령과 자동 도움말로 사용이 쉬움
  • 여러 구성 출처로 유연함
  • 쉘 완성과 적절한 오류 처리로 전문성 있음
  • 깔끔한 코드 구조로 유지보수가 용이함
  • 다양한 플랫폼으로 이식 가능함

개발자 도구, 시스템 유틸리티, DevOps 자동화를 구축하든 간에, Cobra와 Viper는 사용자가 좋아할 CLI 애플리케이션을 만들 수 있는 견고한 기반을 제공합니다.