Bazel 튜토리얼: Go 프로젝트 빌드

이 튜토리얼에서는 Go (Golang) 프로젝트를 빌드하는 방법을 보여주면서 Bazel의 기본사항을 소개합니다. 작업공간을 설정하고, 작은 프로그램을 빌드하고, 라이브러리를 가져오고, 테스트를 실행하는 방법을 알아봅니다. 이 과정에서 대상 및 BUILD 파일과 같은 주요 Bazel 개념을 배우게 됩니다.

예상 완료 시간: 30분

시작하기 전에

Bazel 설치

시작하기 전에 아직 Bazel을 설치하지 않았다면 먼저 설치하세요.

모든 디렉터리에서 bazel version을 실행하여 Bazel이 설치되어 있는지 확인할 수 있습니다.

Go 설치 (선택사항)

Bazel을 사용하여 Go 프로젝트 를 빌드하기 위해 Go를 설치할 필요는 없습니다. Bazel Go 규칙 세트는 머신에 설치된 도구 모음을 사용하는 대신 Go 도구 모음을 자동으로 다운로드하고 사용합니다. 이렇게 하면 프로젝트의 모든 개발자가 동일한 버전의 Go로 빌드할 수 있습니다.

하지만 go getgo mod tidy와 같은 명령어를 실행하기 위해 Go 도구 모음을 설치해야 할 수도 있습니다.

모든 디렉터리에서 go version을 실행하여 Go가 설치되어 있는지 확인할 수 있습니다.

샘플 프로젝트 가져오기

Bazel 예시는 Git 저장소에 저장되므로 아직 Git를 설치하지 않았다면 Git 를 설치해야 합니다. 예시 저장소를 다운로드하려면 다음 명령어를 실행하세요.

git clone https://github.com/bazelbuild/examples

이 튜토리얼의 샘플 프로젝트는 examples/go-tutorial 디렉터리에 있습니다. 포함된 내용을 확인하세요.

go-tutorial/
└── stage1
└── stage2
└── stage3

이 튜토리얼의 각기 다른 섹션에 해당하는 세 개의 하위 디렉터리 (stage1, stage2, stage3)가 있습니다. 각 단계는 이전 단계를 기반으로 합니다.

Bazel을 사용하여 빌드

stage1 디렉터리에서 시작합니다. 여기에서 프로그램을 찾을 수 있습니다. bazel build로 빌드한 후 실행할 수 있습니다.

$ cd go-tutorial/stage1/
$ bazel build //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello_/hello
INFO: Elapsed time: 0.473s, Critical Path: 0.25s
INFO: 3 processes: 1 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions

$ bazel-bin/hello_/hello
Hello, Bazel! 💚

단일 bazel run 명령어로 프로그램을 빌드하고 실행할 수도 있습니다.

$ bazel run //:hello
bazel run //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello_/hello
INFO: Elapsed time: 0.128s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/hello_/hello
Hello, Bazel! 💚

프로젝트 구조 이해

방금 빌드한 프로젝트를 살펴보세요.

hello.go에는 프로그램의 Go 소스 코드가 포함되어 있습니다.

package main

import "fmt"

func main() {
    fmt.Println("Hello, Bazel! 💚")
}

BUILD에는 Bazel에 빌드할 대상을 알려주는 몇 가지 안내가 포함되어 있습니다. 일반적으로 각 디렉터리에 이와 같은 파일을 작성합니다. 이 프로젝트에는 hello.go에서 프로그램을 빌드하는 단일 go_binary 대상이 있습니다.

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "hello",
    srcs = ["hello.go"],
)

MODULE.bazel은 프로젝트의 종속 항목을 추적합니다. 또한 프로젝트의 루트 디렉터리를 표시하므로 프로젝트당 하나의 MODULE.bazel 파일만 작성합니다. Go의 go.mod 파일과 비슷한 용도로 사용됩니다. Bazel 프로젝트에는 실제로 go.mod 파일이 필요하지 않지만 종속 항목 관리에 go getgo mod tidy를 계속 사용할 수 있도록 하는 것이 유용할 수 있습니다. Bazel Go 규칙 세트는 go.mod에서 종속 항목을 가져올 수 있지만 다른 튜토리얼에서 다루겠습니다.

MODULE.bazel 파일에는 Go 규칙 세트인 rules_go에 대한 단일 종속 항목이 포함되어 있습니다. Bazel은 Go를 기본적으로 지원하지 않으므로 이 종속 항목이 필요합니다.

bazel_dep(
    name = "rules_go",
    version = "0.50.1",
)

마지막으로 MODULE.bazel.lock은 Bazel에서 생성한 파일로, 종속 항목에 관한 해시 및 기타 메타데이터가 포함되어 있습니다. Bazel 자체에서 추가한 암시적 종속 항목이 포함되어 있으므로 매우 길며 여기서는 표시하지 않습니다. go.sum과 마찬가지로 프로젝트의 모든 사용자가 동일한 버전의 각 종속 항목을 가져오도록 하려면 MODULE.bazel.lock 파일을 소스 제어에 커밋해야 합니다. MODULE.bazel.lock을 수동으로 수정할 필요는 없습니다.

BUILD 파일 이해

Bazel과의 대부분의 상호작용은 BUILD 파일 (또는 이와 동등한 BUILD.bazel 파일)을 통해 이루어지므로 이러한 파일의 기능을 이해하는 것이 중요합니다.

BUILD 파일은 Python의 제한된 하위 집합인 Starlark라는 스크립트 언어로 작성됩니다.

BUILD 파일에는 대상 목록이 포함되어 있습니다 . 대상은 Bazel에서 빌드할 수 있는 바이너리, 라이브러리 또는 테스트와 같은 항목입니다.

대상은 빌드해야 하는 항목을 설명하기 위해 속성 목록으로 규칙 함수를 호출합니다. 이 예에는 두 가지 속성이 있습니다. name은 명령줄에서 대상을 식별하고 srcs는 소스 파일 경로 목록입니다 (BUILD 파일이 포함된 디렉터리를 기준으로 슬래시로 구분됨).

규칙은 Bazel에 대상을 빌드하는 방법을 알려줍니다. 이 예에서는 go_binary 규칙을 사용했습니다. 각 규칙은 작업 (명령어)을 정의하여 출력 파일 집합을 생성합니다. 예를 들어 go_binary는 실행 가능한 출력 파일을 생성하는 Go 컴파일 및 연결 작업을 정의합니다.

Bazel에는 Java 및 C++와 같은 몇 가지 언어에 관한 기본 제공 규칙이 있습니다. 빌드 백과사전에서 문서를 확인할 수 있습니다. Bazel 중앙 레지스트리(BCR)에서 다른 여러 언어 및 도구의 규칙 세트를 찾을 수 있습니다.

라이브러리 추가

stage2 디렉터리로 이동합니다. 여기에서 운세를 출력하는 새 프로그램을 빌드합니다. 이 프로그램은 미리 정의된 메시지 목록에서 운세를 선택하는 별도의 Go 패키지를 라이브러리로 사용합니다.

go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   └── fortune.go
└── print_fortune.go

fortune.go는 라이브러리의 소스 파일입니다. fortune 라이브러리는 별도의 Go 패키지이므로 소스 파일은 별도의 디렉터리에 있습니다. Bazel에서는 Go 패키지를 별도의 디렉터리에 보관할 필요가 없지만 Go 생태계에서 강력한 규칙이며 이를 따르면 다른 Go 도구와 호환성을 유지하는 데 도움이 됩니다.

package fortune

import "math/rand"

var fortunes = []string{
    "Your build will complete quickly.",
    "Your dependencies will be free of bugs.",
    "Your tests will pass.",
}

func Get() string {
    return fortunes[rand.Intn(len(fortunes))]
}

fortune 디렉터리에는 Bazel에 이 패키지를 빌드하는 방법을 알려주는 자체 BUILD 파일이 있습니다. 여기서는 go_binary 대신 go_library를 사용합니다.

또한 라이브러리를 다른 Go 소스 파일로 가져올 수 있는 문자열로 importpath 속성을 설정해야 합니다. 이 이름은 저장소 내 디렉터리와 연결된 저장소 경로 (또는 모듈 경로)여야 합니다.

마지막으로 visibility 속성을 ["//visibility:public"]으로 설정해야 합니다. visibility 는 모든 대상에 설정할 수 있습니다. 이 대상에 종속될 수 있는 Bazel 패키지를 결정합니다. 이 경우 모든 대상이 이 라이브러리에 종속될 수 있도록 하므로 특수 값 //visibility:public을 사용합니다.

load("@rules_go//go:def.bzl", "go_library")

go_library(
    name = "fortune",
    srcs = ["fortune.go"],
    importpath = "github.com/bazelbuild/examples/go-tutorial/stage2/fortune",
    visibility = ["//visibility:public"],
)

다음을 사용하여 이 라이브러리를 빌드할 수 있습니다.

$ bazel build //fortune

다음으로 print_fortune.go에서 이 패키지를 사용하는 방법을 알아보세요.

package main

import (
    "fmt"

    "github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)

func main() {
    fmt.Println(fortune.Get())
}

print_fortune.gofortune 라이브러리의 importpath 속성에 선언된 동일한 문자열을 사용하여 패키지를 가져옵니다.

또한 이 종속 항목을 Bazel에 선언해야 합니다. stage2 디렉터리의 BUILD 파일은 다음과 같습니다.

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "print_fortune",
    srcs = ["print_fortune.go"],
    deps = ["//fortune"],
)

아래 명령어로 실행할 수 있습니다.

bazel run //:print_fortune

print_fortune 대상에는 종속된 다른 대상 목록인 deps 속성이 있습니다. `fortune` 디렉터리의 `fortune`이라는 대상을 참조하는 라벨 문자열인 `"//fortune"`이 포함되어 있습니다.

Bazel에서는 모든 대상이 deps와 같은 속성으로 종속 항목을 명시적으로 선언해야 합니다. 종속 항목이 소스 파일에도 지정되므로 번거로울 수 있지만 Bazel의 명시성은 이점을 제공합니다. Bazel 은 소스 파일을 읽지 않고 명령어를 실행하기 전에 모든 명령어, 입력, 출력이 포함된 작업 그래프 를 빌드합니다. 그러면 Bazel은 기본 제공 언어별 로직 없이 작업 결과를 캐시하거나 원격 실행을 위해 작업을 전송할 수 있습니다.

라벨 이해

라벨은 Bazel에서 대상 또는 파일을 식별하는 데 사용하는 문자열입니다. 라벨은 명령줄 인수 및 deps와 같은 BUILD 파일 속성에 사용됩니다. 이미 //fortune, //:print-fortune, @rules_go//go:def.bzl과 같은 몇 가지를 살펴보았습니다.

라벨에는 저장소 이름, 패키지 이름, 대상 (또는 파일) 이름의 세 부분이 있습니다.

저장소 이름은 @// 사이에 작성되며 다른 Bazel 모듈의 대상을 참조하는 데 사용됩니다 (과거에는 모듈저장소 가 동의어로 사용되기도 함). 라벨 @rules_go//go:def.bzl에서 저장소 이름은 rules_go입니다. 동일한 저장소의 대상을 참조할 때는 저장소 이름을 생략할 수 있습니다.

패키지 이름은 //: 사이에 작성되며 다른 Bazel 패키지의 대상을 참조하는 데 사용됩니다. 라벨 @rules_go//go:def.bzl에서 패키지 이름은 go입니다. Bazel 패키지는 최상위 디렉터리의 BUILD 또는 BUILD.bazel 파일에 의해 정의된 파일 및 대상 집합입니다. 패키지 이름은 모듈 루트 디렉터리(MODULE.bazel 포함)에서 BUILD 파일이 포함된 디렉터리까지의 슬래시로 구분된 경로입니다. 패키지에는 하위 디렉터리가 포함될 수 있지만 자체 패키지를 정의하는 BUILD 파일이 포함되어 있지 않은 경우에만 가능합니다.

대부분의 Go 프로젝트에는 디렉터리당 하나의 BUILD 파일과 BUILD 파일당 하나의 Go 패키지가 있습니다. 동일한 디렉터리의 대상을 참조할 때는 라벨의 패키지 이름을 생략할 수 있습니다.

대상 이름은 : 뒤에 작성되며 패키지 내의 대상을 참조합니다. 대상 이름이 패키지 이름의 마지막 구성요소와 동일한 경우 생략할 수 있습니다 (//a/b/c:c//a/b/c와 동일하고 //fortune:fortune//fortune와 동일함).

명령줄에서 ...을 와일드 카드로 사용하여 패키지 내의 모든 대상을 참조할 수 있습니다. 이는 저장소의 모든 대상을 빌드하거나 테스트하는 데 유용합니다.

# Build everything
$ bazel build //...

프로젝트 테스트

다음으로 stage3 디렉터리로 이동합니다. 여기에서 테스트를 추가합니다.

go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   ├── fortune.go
│   └── fortune_test.go
└── print-fortune.go

fortune/fortune_test.go는 새로운 테스트 소스 파일입니다.

package fortune

import (
    "slices"
    "testing"
)

// TestGet checks that Get returns one of the strings from fortunes.
func TestGet(t *testing.T) {
    msg := Get()
    if i := slices.Index(fortunes, msg); i < 0 {
        t.Errorf("Get returned %q, not one the expected messages", msg)
    }
}

이 파일은 내보내지 않은 fortunes 변수를 사용하므로 fortune.go와 동일한 Go 패키지로 컴파일해야 합니다. BUILD 파일을 보고 작동 방식을 확인하세요.

load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
    name = "fortune",
    srcs = ["fortune.go"],
    importpath = "github.com/bazelbuild/examples/go-tutorial/stage3/fortune",
    visibility = ["//visibility:public"],
)

go_test(
    name = "fortune_test",
    srcs = ["fortune_test.go"],
    embed = [":fortune"],
)

테스트 실행 파일을 컴파일하고 연결하는 데 go_test 규칙을 사용하는 새로운 fortune_test 대상이 있습니다. go_testfortune.gofortune_test.go를 동일한 명령어로 함께 컴파일해야 하므로 여기서는 embed 속성을 사용하여 fortune 대상의 속성을 fortune_test에 통합합니다. embedgo_testgo_binary와 함께 가장 흔히 사용되지만 go_library에서도 작동하며 이는 생성된 코드에 유용할 수 있습니다.

embed 속성이 실행 파일에 복사된 데이터 파일에 액세스하는 데 사용되는 Go's embed 패키지와 관련이 있는지 궁금할 수 있습니다. 이는 불행한 이름 충돌입니다. rules_go의 embed 속성은 Go의 embed 패키지보다 먼저 도입되었습니다. 대신 rules_go 는 embedsrcs 패키지로 로드할 수 있는 파일을 나열하는 데 embed를 사용합니다.

bazel test로 테스트를 실행해 보세요.

$ bazel test //fortune:fortune_test
INFO: Analyzed target //fortune:fortune_test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //fortune:fortune_test up-to-date:
  bazel-bin/fortune/fortune_test_/fortune_test
INFO: Elapsed time: 0.168s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//fortune:fortune_test                                          PASSED in 0.3s

Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.

... 와일드 카드를 사용하여 모든 테스트를 실행할 수 있습니다. Bazel은 테스트가 아닌 대상도 빌드하므로 테스트가 없는 패키지에서도 컴파일 오류를 포착할 수 있습니다.

$ bazel test //...

결론 및 추가 자료

이 튜토리얼에서는 Bazel을 사용하여 작은 Go 프로젝트를 빌드하고 테스트했으며 이 과정에서 몇 가지 핵심 Bazel 개념을 배웠습니다.

  • Bazel을 사용하여 다른 애플리케이션을 빌드하려면 C++, Java, Android, iOS 튜토리얼을 참고하세요.
  • 다른 언어의 권장 규칙 목록을 확인할 수도 있습니다.
  • Go에 관한 자세한 내용은 rules_go 모듈, 특히 핵심 Go 규칙 문서를 참고하세요.
  • 프로젝트 외부에서 Bazel 모듈로 작업하는 방법을 자세히 알아보려면 외부 종속 항목을 참고하세요. 특히 Bazel의 모듈 시스템을 통해 Go 모듈 및 도구 모음에 종속되는 방법에 관한 정보는 bzlmod를 사용한 Go를 참고하세요.