제가 사용하는 기본적인 Go 프로젝트 레이아웃에 대해 간략히 소개하기 위해 해당 포스팅을 작성하게 되었습니다.


목차

  1. 1. 프로젝트 레이아웃이 필요한 이유는?
  2. 2. Go 코드를 어떻게 작성해야하는가?
    1. 2.1. Code organization
    2. 2.2. Your first program
    3. 2.3. Importing packages from your module
    4. 2.4. Importing packages from remote modules
  3. 3. 내가 사용하는 Go 프로젝트 레이아웃
    1. 3.1. bin/
    2. 3.2. cmd/
    3. 3.3. internal/
    4. 3.4. internal/platform/
  4. 4. 프로젝트 예시

프로젝트 레이아웃이 필요한 이유는?

최초 프로젝트 설계자가 아닌 또 다른 누군가가 유지보수를 담당하게되는 경우(대부분의 소프트웨어 개발 회사의 상황일 것으로 생각됨) 프로젝트 레이아웃이 명확하지 않은 경우에는 전체 구조를 파악하기가 어려울 뿐만아니라 의도치 않은 방향으로 소스 코드가 추가될 수 있습니다.


Go 코드를 어떻게 작성해야하는가?

먼저 Golang 공식 홈페이지에서 제공하고 있는 Go 코드 작성 가이드를 통해 패키지(package)와 모듈(module)에 대한 개념을 먼저 잡아보도록 하겠습니다. (해당 링크를 통해 원문을 확인하실 수 있습니다.)

Code organization

Go 프로그램은 패키지들로 구성됩니다. 여기서 *패키지(package)*란 함께 컴파일되는 동일 디렉토리 내의 소스 파일들의 집합입니다. 패키지 내 하나의 소스 파일 안에 정의된 함수, 타입, 변수 그리고 상수들은 동일한 패키지 내에 있는 다른 모든 소스 파일들에서 접근이 가능합니다.

Go programs are organized into packages. A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.

하나의 저장소는 한 개 이상의 모듈들을 포함하고 있습니다. 여기서 *모듈(module)*이란 함께 배포되는 연관된 Go 패키지들의 집합입니다. Go 저장소는 일반적으로 하나의 모듈만을 포함하며 이 모듈은 저장소의 root 경로에 존재합니다. 저장소의 root 경로에 go.mod 라는 이름의 파일에서는 모듈 경로를 선언해주는데 이 경로는 모듈 내에 존재하는 모든 패키지들의 임포트 경로의 접두사(prefix)로 사용됩니다. 모듈은 go.mod 파일이 포함된 디렉토리에 존재하는 패키지들뿐만아니라 그 디렉토리들의 하위 디렉토리에 존재하는 패키지들까지 포함합니다.(만약 go.mod 파일이 두 개 이상 존재하는 경우라면 또 다른 go.mod 파일이 존재하는 다음 하위 디렉토리 전까지를 포함함)

A repository contains one or more modules. A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository. A file named go.mod there declares the module path: the import path prefix for all packages within the module. The module contains the packages in the directory containing its go.mod file as well as subdirectories of that directory, up to the next subdirectory containing another go.mod file (if any).

여러분의 소스 코드를 빌드하기 전에 이것을 원격 저장소에 배포할 필요는 없습니다. 모듈은 원격 저장소에서 관리되지 않더라도 로컬에서 정의될 수 있습니다. 하지만 미래의 배포 가능성을 열어두고 여러분의 소스 코드를 조직화하는 것은 좋은 습관입니다.

Note that you don’t need to publish your code to a remote repository before you can build it. A module can be defined locally without belonging to a repository. However, it’s a good habit to organize your code as if you will publish it someday.

각 모듈의 경로 정보는 해당 모듈에 포함된 패키지들의 임포트 경로의 접두사(prefix)를 제공할 뿐만 아니라, go 명령어가 해당 모듈을 다운로드하기 위해 바라봐야할 위치를 가리킵니다. 예를 들어, golang.org/x/tools 라는 모듈을 다운로드 하기위해서는 go 명령어가 https://golang.org/x/tools가 가리키는 저장소를 참고할 것입니다.

Each module’s path not only serves as an import path prefix for its packages, but also indicates where the go command should look to download it. For example, in order to download the module golang.org/x/tools, the go command would consult the repository indicated by https://golang.org/x/tools (described more here).

임포트 경로는 패키지를 임포트하기 위해 사용됩니다. 패키지의 임포트 경로는 해당 패키지의 모듈 경로와 모듈 내에 존재하는 연결된 하위 디렉토리 경로입니다. 에를 들어, github.com/google/go-cmp 모듈은 cmp/ 디렉토리 안의 패키지를 포함합니다. 그 패키지의 임포트 경로는 github.com/google/go-cmp/cmp 입니다. 스탠다드 라이브러리 내의 패키지들은 모듈 경로 접두사를 가지고 있지 않습니다.

An import path is a string used to import a package. A package’s import path is its module path joined with its subdirectory within the module. For example, the module github.com/google/go-cmp contains a package in the directory cmp/. That package’s import path is github.com/google/go-cmp/cmp. Packages in the standard library do not have a module path prefix.

Your first program

단순한 프로그램을 컴파일하고 실행하기 위해 먼저 모듈 경로(example/user/hello라는 경로를 사용할 예정)를 선택하고, 그 경로를 선언하고 있는 go.mod 파일을 생성합니다.

To compile and run a simple program, first choose a module path (we’ll use example/user/hello) and create a go.mod file that declares it:

1
2
3
4
5
6
7
8
9
$ mkdir hello # Alternatively, clone it if it already exists in version control.
$ cd hello
$ go mod init example/user/hello
go: creating new go.mod: module example/user/hello
$ cat go.mod
module example/user/hello

go 1.16
$

Go 소스 파일의 첫번째 문장으로는 패키지 이름이 작성돼야합니다. 실행 가능한 명령어는 항상 main 패키지를 사용해야 합니다.

The first statement in a Go source file must be package name. Executable commands must always use package main.

다음으로 해당 디렉토리 내에 아래 Go 코드를 포함하는 hello.go 파일을 생성합니다.

Next, create a file named hello.go inside that directory containing the following Go code:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello, world.")
}

이제 여러분은 go tool을 사용해서 프로그램을 빌드하고 설치할 수 있습니다.

Now you can build and install that program with the go tool:

1
$ go install example/user/hello

이 명령어는 hello 명령어를 빌드하고, 실행가능한 바이너리를 생성합니다. 그리고나서 그 바이너리 파일을 $HOME/go/bin/hello 경로에 설치합니다.

This command builds the hello command, producing an executable binary. It then installs that binary as $HOME/go/bin/hello.

설치 경로는 GOPATH와 GOBIN 환경 변수를 통해 조절할 수 있습니다. 만약 GOBIN 환경 변수가 설정이되었다면, 바이너리 파일은 그 경로에 설치될것입니다. 만약 GOPATH가 설정되었다면, 바이너리 파일은 GOPATH 리스트의 첫번째 디렉토리의 bin 이라는 하위 디렉토리에 설치될 것 입니다. 두 환경 변수 모두 설정되지 않은 경우라면, 바이너리 파일은 기본 GOPATH 경로($HOME/go)에 설치될 것 입니다.

The install directory is controlled by the GOPATH and GOBIN environment variables. If GOBIN is set, binaries are installed to that directory. If GOPATH is set, binaries are installed to the bin subdirectory of the first directory in the GOPATH list. Otherwise, binaries are installed to the bin subdirectory of the default GOPATH ($HOME/go).

여러분은 추후에 사용될 go 커맨드가 참조할 수 있는 환경 변수의 기본 값을 go env 명령어를 통해 설정할 수 있습니다.

You can use the go env command to portably set the default value for an environment variable for future go commands:

1
$ go env -w GOBIN=/somewhere/else/bin

go env -w를 통해 이전에 설정한 값을 해제하고 싶은 경우에는 go env -u를 사용하면 됩니다.

To unset a variable previously set by go env -w, use go env -u:

1
$ go env -u GOBIN

go install과 같은 명령어는 현재 작업 경로를 포함하고 있는 모듈의 컨텍스트 내에 적용됩니다. 만약 작업 경로가 example/user/hello 모듈 내부가 아니라면, go insall 명령어는 실패할 것 입니다.

Commands like go install apply within the context of the module containing the current working directory. If the working directory is not within the example/user/hello module, go install may fail.

편리성을 위해 go 명령어는 작업 경로에 상대적인 경로를 허용하고, 다른 경로가 주언진 경우가 아니라면 현재 작업 경로의 패키지를 기본으로 사용합니다. 그래서 현재 우리의 작업 경로에서는, 아래 명령어들이 모두 동일한 결과를 갖습니다.

For convenience, go commands accept paths relative to the working directory, and default to the package in the current working directory if no other path is given. So in our working directory, the following commands are all equivalent:

1
$ go install example/user/hello
1
$ go install .
1
$ go install

다음으로 정상적으로 동작하는지 확인해보기 위해 프로그램을 실행해봅시다. 바이너리를 쉽게 실행하기 위해 PATH 환경 변수에 설치 경로를 추가할 것 입니다.

Next, let’s run the program to ensure it works. For added convenience, we’ll add the install directory to our PATH to make running binaries easy:

1
2
3
4
5
6
# Windows users should consult /wiki/SettingGOPATH
# for setting %PATH%.
$ export PATH=$PATH:$(dirname $(go list -f '{{.Target}}' .))
$ hello
Hello, world.
$

Importing packages from your module

morestrings 패키지를 작성하고 그 패키지를 hello 프로그램에서 사용하도록 해봅시다. 먼저 해당 패키지를 저장할 $HOME/hello/morestrings 디렉토리를 생성해줍니다. 그리고나서 아래 내용을 포함하는 reverse.go 라는 파일을 그 디렉토리 내에서 생성해줍시다.

Let’s write a morestrings package and use it from the hello program. First, create a directory for the package named $HOME/hello/morestrings, and then a file named reverse.go in that directory with the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
// Package morestrings implements additional functions to manipulate UTF-8
// encoded strings, beyond what is provided in the standard "strings" package.
package morestrings

// ReverseRunes returns its argument string reversed rune-wise left to right.
func ReverseRunes(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}

ReverseRunes() 함수는 대문자로 시작하기 때문에 morestring 패키지를 임포트한 또 다른 패키지들에서 해당 함수를 사용할 수 있습니다.

Because our ReverseRunes function begins with an upper-case letter, it is exported, and can be used in other packages that import our morestrings package.

go build로 morestrings 패키지를 컴파일 해봅시다.

Let’s test that the package compiles with go build:

1
2
$ cd $HOME/hello/morestrings
$ go build

위 명령어로는 어떠한 결과 파일이 생성되지는 않을 것입니다. 대신에 그것은 로컬 빌드 캐시에 컴파일된 패키지를 저장합니다.

This won’t produce an output file. Instead it saves the compiled package in the local build cache.

morestrings 패키지 빌드 이후에, hello 프로그램에서 이것을 사용해봅시다. 이를 위해 $HOME/hello/hello.go 코드에서 morestrings 패키지를 사용할 수 있도록 수정해봅시다.

After confirming that the morestrings package builds, let’s use it from the hello program. To do so, modify your original $HOME/hello/hello.go to use the morestrings package:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"

"example/user/hello/morestrings"
)

func main() {
fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
}

hello 프로그램을 설치하고

Install the hello program:

1
$ go install example/user/hello

새로운 버전의 hello 프로그램을 실행해보면 여러분은 뒤집힌 메시지를 볼 수 있을겁니다.

Running the new version of the program, you should see a new, reversed message:

1
2
$ hello
Hello, Go!

Importing packages from remote modules

임포트 경로는 버전 관리 시스템인 Git 이나 Mercurial을 사용하는 패키지 소스 코드를 어떻게 받아오는지를 설명할 수 있습니다. go tool은 원격 저장소에 있는 패키지들을 자동으로 가져오기 위해 이러한 특성을 사용합니다.

An import path can describe how to obtain the package source code using a revision control system such as Git or Mercurial. The go tool uses this property to automatically fetch packages from remote repositories. For instance, to use github.com/google/go-cmp/cmp in your program:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"

"example/user/hello/morestrings"
"github.com/google/go-cmp/cmp"
)

func main() {
fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
fmt.Println(cmp.Diff("Hello World", "Hello Go"))
}

이제 여러분은 외부 모듈에 대한 의존성을 가지게 되었고, 그러한 모듈을 다운로드하고 그것의 버전 정보를 go.mode 파일 내에 기록할 필요가 있습니다. go mod tidy 명령어는 임포트 패키지들에 대한 누락된 모듈 요구사항들을 추가해주고 더이상 사용하지 않는 모듈들에 대한 요구사항들을 제거해줍니다.

Now that you have a dependency on an external module, you need to download that module and record its version in your go.mod file. The go mod tidy command adds missing module requirements for imported packages and removes requirements on modules that aren’t used anymore.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ go mod tidy
go: finding module for package github.com/google/go-cmp/cmp
go: found github.com/google/go-cmp/cmp in github.com/google/go-cmp v0.5.4
$ go install example/user/hello
$ hello
Hello, Go!
string(
- "Hello World",
+ "Hello Go",
)
$ cat go.mod
module example/user/hello

go 1.16

require github.com/google/go-cmp v0.5.4
$

모듈 의존성은 GOPATH 환경 변수가 가리키는 경로의 하위 경로인 pkg/mod에 자동으로 다운로드 됩니다. 다운로드된 모듈의 버전 정보들은 해당 버전을 필요로 하는 다른 모든 모듈들에게 공유됩니다. 그래서 go 명령어는 이러한 파일과 디렉토리를 read-only로 표시합니다. 다운로드 받은 모든 모듈들을 삭제하기 위해서는 go clean 명령어에 -modcache 플래그를 전달해주면 됩니다.

Module dependencies are automatically downloaded to the pkg/mod subdirectory of the directory indicated by the GOPATH environment variable. The downloaded contents for a given version of a module are shared among all other modules that require that version, so the go command marks those files and directories as read-only. To remove all downloaded modules, you can pass the -modcache flag to go clean:

1
$ go clean -modcache

내가 사용하는 Go 프로젝트 레이아웃

다양한 프로젝트 레이아웃들을 비교해본 끝에 Package Oriented Design라는 포스팅을 참조하여 프로젝트 레이아웃을 구성해보기로 결정했고, 실제 현업에서 개발 중인 서비스를 이 구조에 맞춰서 구현해나갔습니다.

bin/

  • 실행가능한 바이너리 파일이 생성되는 경로
  • 바이너리 실행을 위해 필요한 설정 값이 저장되는 config 파일도 포함됨

cmd/

  • main 패키지를 포함하는 go 파일이 저장되는 경로

internal/

  • main 패키지에서 임포트하는 패키지들의 집합이며, 제공하는 서비스 별로 패키지들을 구분함(ex. internal/gateway)

동일 프로젝트 내에 존재하는 2개 이상의 프로그램들에서 임포트될 필요성이 있는 패키지들이 internal/ 폴더 내에 속해있습니다. internal/ 이라는 명칭의 사용이 가져다 주는 한가지 이점은 프로젝트가 컴파일러로부터 추가적인 보호 레벨을 얻어낼 수 있다는 점입니다. internal/ 폴더 내에 존재하는 패키지들을 해당 프로젝트의 외부에 있는 패키지에서 임포트할 수 없습니다. 그러므로 이러한 패키지들은 오로지 해당 프로젝트 내에 귀속됩니다.

Packages that need to be imported by multiple programs within the project belong inside the internal/ folder. One benefit of using the name internal/ is that the project gets an extra level of protection from the compiler. No package outside of this project can import packages from inside of internal/. These packages are therefore internal to this project only.

internal/platform/

  • internal에 포함된 패키지들에서 공통적으로 사용할 수 있는 기능을 제공하는 패키지들의 집합

기반이되는 그러나 프로젝트에 특정되는 패키지들이 internal/platform 폴더 내에 속해있습니다. 이러한 패키지들은 데이터베이스나 인증 또는 marshaling과 같은 기능들을 제공하곤 합니다.

Packages that are foundational but specific to the project belong in the internal/platform/ folder. These would be packages that provide support for things like databases, authentication or even marshaling.


프로젝트 예시

현재 토이 프로젝트로 진행중인 go 서비스에서도 위 프로젝트 레이아웃을 사용 중에 있습니다. 해당 링크를 통해 확인 가능하십니다.



해당 게시글에서 발생한 오탈자나 잘못된 내용에 대한 정정 댓글 격하게 환영합니다😎

Reference