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 같은 이름은 피한다. 구체적인 역할로 명명