Go Project Layout
Go 프로젝트의 디렉토리 구조를 잡는 두 가지 대표적인 패턴을 정리한다.
1. Layer-based (기술 역할 기준)
internal/
├── handler/
│ ├── user.go
│ └── order.go
├── service/
│ ├── user.go
│ └── order.go
├── repository/
│ ├── user.go
│ └── order.go
└── router/
└── router.go
같은 기술 역할끼리 묶는 방식. Spring 등 다른 프레임워크에서 넘어온 개발자에게 익숙하다.
Pros
- 구조가 직관적이고 진입 장벽이 낮음
- 같은 레이어 간 코드 재사용이 쉬움 (공통 middleware 등)
- 소규모 프로젝트에서 심플하고 빠름
Cons
- 기능 하나 수정 시 여러 디렉토리를 왔다갔다 해야 함 (handler/ + service/ + repository/)
- 도메인이 커지면 각 디렉토리 안에 파일이 폭발적으로 늘어남
- 패키지 간 순환 의존성이 생기기 쉬움
2. Domain-based (비즈니스 도메인 기준)
internal/
├── user/
│ ├── handler.go
│ ├── service.go
│ └── repository.go
├── order/
│ ├── handler.go
│ ├── service.go
│ └── repository.go
└── router.go
같은 도메인끼리 묶는 방식. Go 커뮤니티에서 더 권장하는 패턴.
Pros
- 도메인 응집도가 높음. user 관련 코드는 전부
user/안에 - 기능 수정 시 한 디렉토리 안에서 거의 끝남
- 도메인 간 경계가 명확해서 순환 의존성이 생기기 어려움
- 마이크로서비스 분리 시 디렉토리 단위로 떼어내기 쉬움
Cons
- 도메인 간 공통 로직 배치가 애매함 (shared? common? platform?)
- 작은 프로젝트에서는 오버엔지니어링으로 느껴질 수 있음
- 도메인 경계를 잘못 잡으면 오히려 더 복잡해짐
3. 비교
| Layer-based | Domain-based | |
|---|---|---|
| 기준 | 기술 역할 (handler, service) | 비즈니스 도메인 (user, order) |
| 응집도 | 같은 역할끼리 | 같은 도메인끼리 |
| 파일 탐색 | "handler 어디있지?" → 쉬움 | "user 관련 전부?" → 쉬움 |
| 확장성 | 도메인 늘어나면 파일 폭발 | 디렉토리 추가로 깔끔하게 확장 |
| 적합한 규모 | 소~중규모, CRUD 위주 | 중~대규모, 도메인이 복잡할 때 |
4. 공통 디렉토리
어떤 패턴을 쓰든 일반적으로 사용하는 최상위 디렉토리 구조:
project/
├── cmd/
│ └── myapp/
│ └── main.go # 진입점. 최소한의 코드만 둔다
├── internal/ # 외부 패키지에서 import 불가 (Go 컴파일러가 강제)
│ └── ...
├── pkg/ # 외부에서도 import 가능한 라이브러리 (선택)
├── migrations/ # DB migration 파일
├── config/ # 설정 파일
├── Makefile
├── go.mod
└── go.sum
cmd/— 바이너리 진입점. 여러 바이너리가 있으면cmd/api/,cmd/worker/등으로 분리internal/— Go가 언어 레벨에서 접근을 제한하는 디렉토리. 외부 모듈에서 import 시 컴파일 에러pkg/— 외부에 공개할 라이브러리. 없어도 됨. 최근에는 안 쓰는 추세
5. 실무 팁
- 처음에는 layer-based로 시작하고, 도메인이 복잡해지면 domain-based로 전환하는 경우가 많다
- 디렉토리를 너무 깊게 중첩하지 않는다. Go는 flat한 구조를 선호
- 패키지 이름에
util,common,helper같은 이름은 피한다. 구체적인 역할로 명명