Go의 비밀스러운 삶: 패키지와 구조
I’m happy to help translate the article, but I’ll need the full text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source link at the top and preserve all formatting, markdown, and code blocks as requested.
제11장: 설계자의 청사진
금요일 오후 햇살이 아카이브 창을 통해 낮게 비추며 공중에 떠다니는 먼지 입자들을 비추었다. 이선은 사다리 위에 서서 가죽 제본 원장을 조심스럽게 재배치하고 있는 엘리노어를 발견했다.
Ethan: “정리하고 있는 거야?”
그는 종이봉지를 책상 위에 놓았다.
Eleanor: “수정하고 있어.” 그녀는 만족스러운 한숨을 쉬며 무거운 책 한 권을 제자리에 끼워 넣었다. “이전 아키비스트가 ‘제본 색상’ 기준으로 파일을 정리했지, ‘구입 연도’ 기준이 아니라. 보기엔 예뻤지만 검색은 힘들었어. 뭘 찾으려면 여기저기 다 찾아야 했거든.”
그녀는 내려와 손에 묻은 먼지를 털어냈다.
Ethan: “뭐 가져왔어?”
“피스타치오 크루아상. 그리고 코르타도.”
엘리노어의 표정이 진심 어린 미소로 부드러워졌다.
Eleanor: “코르타도를 기억했구나. 고마워, Ethan.” 그녀는 맞은편 의자를 가리켰다. “앉아. 따뜻한 음료를 마시기 딱 좋은 오후야.”
이선은 노트북을 열었다.
Ethan: “네가 제안한 인벤토리 시스템을 만들었어. 완벽히 동작해. 원자 연산은 빠르고, 뮤텍스는 안전해. 배포할 준비가 됐어.”
엘리노어는 화면을 들여다보며 관심 깊게 몸을 기울였다.
Eleanor: “한 번 보자. 로직만 보는 게 아니라, 네가 만든 ‘집’도 함께 보자.”
그녀는 왼쪽에 있는 파일 탐색기를 가리켰다.
/my-project
├── main.go
├── user.go
├── product.go
├── db.go
├── server.go
├── utils.go
└── types.go
Eleanor: “이 아카이브를 처음 정리했을 때가 떠오르네. 모든 것을 한 곳에 두면 처음엔 접근하기 쉬워 보이지. 모든 게 바로 눈앞에 있잖아.”
Ethan: “그래, 바로 그거야. 폴더를 뒤적일 필요가 없으니까.”
Eleanor: “하지만 이 도서관에 구역이 하나도 없고, 바닥 한가운데에 거대한 책 더미만 있다면 어떨까? 컬렉션이 커질수록 필요한 것을 찾는 게 큰 부담이 돼. Go에서는 파일 시스템 자체가 아키텍처야. 패키지에 대해 이야기해야 해.”
캡슐화 경계
Eleanor: “많은 언어에서 폴더는 단지 조직을 위한 것이야. Go에서는 폴더가 바로 패키지야. 이것은 경계—마치 안전한 독서실과 같지.”
그녀는 새 터미널을 열었다.
Eleanor: “같은 디렉터리 안의 모든 파일은 반드시 같은
package이름을 선언해야 해. 함께 살아가고, 모든 것을 공유하지. 하지만 외부 세계에겐? 보여주고 싶은 것만 보여줘.”
Ethan: “공개와 비공개를 말하는 거야?”
Eleanor: “Go에서는 Exported와 Unexported라고 해. 그리고 멋진 점은
public이나private같은 키워드가 없다는 거야. 오직 대소문자 규칙만 존재하지.”
그녀는 간단한 예시를 입력했다:
package users
// Exported (Public) - Starts with Capital Letter
type User struct {
Name string // Exported field
id int // Unexported field (private)
}
// Exported function
func New(name string) *User {
return &User{
Name: name,
id: generateID(), // We can call unexported functions inside the package
}
}
// Unexported function (Private) - Starts with lowercase
func generateID() int {
return 42 // Simplified
}
Eleanor: “대문자로 시작하면 다른 패키지가 볼 수 있어. 소문자로 시작하면 이 패키지 안에서만 보이게 돼. 간단하고 우아하지. API를 알기 위해 설정 파일을 볼 필요 없이 대소문자를 보면 돼.”
이선은 고개를 끄덕였다.
Ethan: “알겠어. 그러면 패키지별로 그룹화해야겠네.
models패키지와controllers패키지를 만들게.”
엘리노어는 그를 멈추게 하려는 것이 아니라 안내하려는 듯 손을 들어 올렸다.
Eleanor: “조심해. 그건 다른 프레임워크에서 온 습관이야. Go에서는 종류별로 묶지 않아—모든 모델을 한 곳에 두거나 모든 컨트롤러를 한 곳에 두는 식으로 말이야. 우리는 책임별로 묶어야 해.”
그녀는 메모장에 다이어그램을 스케치했다.
BAD ARCHITECTURE (Group by Kind):
/models
- user.go
- product.go
/controllers
- user.go
- product.go
GOOD ARCHITECTURE (Group by Domain):
/users
- user.go
- handler.go
- repositor
Eleanor: “차이점이 보이시나요? 도메인별로 그룹화하면 Users와 관련된 모든 것이
package users안에 들어갑니다. 이것은 독립적인 구조이며, 소프트웨어가 무엇을 하는지에 대한 이야기를 들려줍니다. 단순히 어떻게 만들어졌는지에 대한 이야기가 아니라요.”
내부 요새
Eleanor: “이제 곧 여러분의 패키지들 사이에서 공유해야 할 코드—헬퍼나 핵심 로직—가 생기겠지만, 그 코드를 외부에 공개하고 싶지는 않을 겁니다. 예를 들어 사용자와 제품을 위한 특수 암호화 헬퍼 같은 경우죠.”
Ethan: “
shared패키지를 만들면 될까요?”
Eleanor: “그럴 수도 있죠. 하지만 그렇게 하면 여러분의 라이브러리를 가져가는 모든 사람이
shared패키지를 사용할 수 있게 됩니다. 이는 여러분의 공개 약속의 일부가 되며, 이를 변경하면 사용자를 깨뜨릴 위험이 있습니다.”
그녀가 화면을 두드렸다.
Eleanor: “Go는 이를 위해 멋진 도구,
internal을 제공합니다.”
그녀는 화면에 Ethan의 프로젝트 구조를 다음과 같이 재배치했다:
/my-project
├── go.mod
├── main.go
├── internal
│ └── platform
│ └── crypto.go // my-project 내부에서만 보입니다!
├── users
│ ├── service.go // internal/platform을 임포트할 수 있음
│ └── user.go
└── products
└── product.go
Eleanor: “Go 컴파일러가 이를 정확히 강제합니다.
internal이라는 디렉터리 안에 있는 모든 패키지는 부모 디렉터리를 기준으로 한 루트에 있는 코드만 임포트할 수 있습니다. 이렇게 하면 외부에서는 절대 건드릴 수 없는, 필요하지만 지저분한 세부 사항을 작성할 수 있습니다.”
Ethan: “그럼
internal은 우리만을 위한 건가요?”
Eleanor: “바로 우리만을 위한 것이죠. 나중에 리팩터링을 두려움 없이 할 수 있게 해주는 안전망입니다.”
“Utils” 질문
Ethan은 화면에 새로 정리된 구조를 바라보며 질서가 잡히는 느낌을 받았다. 그리고 파일 목록을 가리키며 물었다.
Ethan: “그럼…
utils.go는요? 문자열 포맷팅, 몇 가지 수학 연산, 난수 생성기 등이 들어있어요.”
Eleanor는 동정어린 미소를 지으며 대답했다.
Eleanor: “아,
utils…* (대화는 계속됩니다)*
Source: https://www.tech-reader.blog/
패키지에 관한 대화
“우리는 결국 모두 패키지를 만들게 돼요. 잡동사니를 넣을 곳이 필요하다는 자연스러운 충동이 있죠. 하지만 그걸 정리함이라고 생각해 보세요. 한 번 넣기 시작하면 멈추지 않고, 다시는 찾지 못하게 돼요.”
“그럼 삭제하라는 건가요?”
“아니요, 배포하세요,” 그녀가 부드럽게 고쳤다. “제대로 된 집을 찾아 주세요. 함수가 사용자 이름을 포맷한다면
users에 넣고, 세금을 계산한다면billing에 넣으세요. 패키지 이름은 무엇을 제공하는지를 설명해야지, 단순히 ‘뭐든지 들어있다’는 식으로 하면 안 돼요.”
그녀는 크루아상을 한 입 물고 고개를 끄덕였다.
“정말 맛있어요, Ethan.”
그녀는 노트북에서 부스러기를 털어냈다.
“구조는 파일을 정렬하는 것만이 아니에요. 의존성 사이클을 방지하도록 시스템을 설계하는 것이죠.
패키지 A가패키지 B를 필요로 하고,패키지 B가 다시패키지 A를 필요로 하면 Go는 컴파일을 거부합니다. 평평한 디렉터리는 그런 사이클을 숨깁니다. 구조화된 프로젝트는 사이클을 드러내어 고칠 수 있게 해 줍니다.”
Ethan은 users 디렉터리를 만들기 시작했다.
“이제 잡동사니 서랍은 없겠군요.”
“모든 것이 제자리에, 제자리에 모든 것이 있죠,” Eleanor가 따뜻하게 동의했다. “그리고 내보내는 이름은 대문자로 시작하세요. 당신은 일기를 쓰는 것이 아니라 라이브러리를 만들고 있는 겁니다. 세상이 읽을 수 있게 하세요.”
제11장 핵심 개념
Packages
- Go에서 코드 조직의 기본 단위.
- 모든 디렉터리는 하나의 패키지.
- 같은 디렉터리 안의 모든 파일은 동일한
package이름을 선언해야 함.
Exported vs. Unexported
| Symbol | Visibility | Example |
|---|---|---|
대문자로 시작 (예: User) | Exported (public) | 다른 패키지에서도 사용 가능 |
소문자로 시작 (예: user) | Unexported (private) | 같은 패키지 내부에서만 사용 가능 |
Domain‑Driven Structure
- 도메인(예:
users,orders,billing) 기준으로 패키지를 구성하고, 레이어(예:models,controllers,utils) 기준보다는 선호합니다. - 관련 로직을 함께 묶어 둡니다.
internal 디렉터리
- Go 툴체인이 인식하는 특수 디렉터리.
internal안의 코드는 부모 디렉터리 이하에 있는 코드에서만 import 할 수 있음.- 구현 세부 사항을 외부 사용자로부터 보호합니다.
Package Naming
- 짧고 간결하며 소문자 사용 (예:
net,http,json). utils,common,helpers같은 일반적인 이름은 피하세요.- 패키지 이름은 그 패키지가 무엇을 제공하는지를 설명해야 함.
Circular Dependencies
- Go는 import 사이클(A가 B를, B가 A를 import) 을 허용하지 않음.
- 올바른 패키지 구조는 단방향 의존성을 강제해 사이클을 방지합니다.
go.mod
- 모듈을 정의하고 의존성을 관리함.
- 프로젝트 루트에 위치합니다.
다음 장: Error Handling — Eleanor가 Go에 예외가 없는 이유를 설명하고, Ethan이 오류를 값으로 다루는 방법을 배우는 내용.
Aaron Rose는 tech-reader.blog의 소프트웨어 엔지니어이자 기술 작가이며, Think Like a Genius의 저자입니다.