Go 프로젝트 구조: 관행 및 패턴
확장성과 명확성을 위해 Go 프로젝트 구조화하기
Go 프로젝트 구조화는 장기적인 유지 보수성, 팀 협업 및 확장성의 근본적인 요소입니다. 엄격한 디렉토리 레이아웃을 강제하는 프레임워크와 달리, Go는 유연성을 수용합니다. 하지만 이러한 자유에는 프로젝트의 특정 요구 사항을 충족하는 패턴을 선택할 책임이 따릅니다.

프로젝트 구조에 대한 Go의 철학 이해하기
Go의 최소주의적 디자인 철학은 프로젝트 조직에도 확장됩니다. 이 언어는 특정 구조를 규정하지 않으며, 대신 개발자가 정보에 기반한 결정을 내릴 수 있도록 신뢰합니다. 이러한 접근 방식은 커뮤니티가 소규모 프로젝트를 위한 간단한 플랫 레이아웃부터 엔터프라이즈 시스템을 위한 정교한 아키텍처까지 여러 검증된 패턴을 개발하게 되었습니다.
핵심 원칙은 단순함을 우선하고, 필요할 때 복잡함을 추가하는 것입니다. 많은 개발자들은 초기 구조를 과잉 설계하는 함정에 빠져 깊게 중첩된 디렉토리와 조숙한 추상화를 만듭니다. 오늘 필요한 것부터 시작하고 프로젝트가 성장함에 따라 리팩토링하십시오.
internal/ 디렉토리와 pkg/ 디렉토리 중 언제 사용해야 할까요?
internal/ 디렉토리는 Go 디자인에서 특정 목적을 수행합니다. 외부 프로젝트에서 가져올 수 없는 패키지를 포함합니다. Go 컴파일러가 이 제한을 강제하므로 internal/은 프로젝트 외부에서 재사용할 의도가 없는 프라이빗 애플리케이션 로직, 비즈니스 규칙 및 유틸리티에 완벽합니다.
반면 pkg/ 디렉토리는 코드가 외부 소비를 위해 의도되었음을 나타냅니다. 라이브러리를 빌드하거나 다른 사람들이 가져오기를 원하는 재사용 가능한 컴포넌트만 여기에 배치하십시오. 많은 프로젝트는 pkg/가 전혀 필요하지 않습니다. 코드가 외부에서 소비되지 않는다면 루트 또는 internal/에 유지하는 것이 더 깔끔합니다. 재사용 가능한 라이브러리를 빌드할 때는 Go 제네릭을 활용하여 타입 안전하고 재사용 가능한 컴포넌트를 만드는 것을 고려하십시오.
표준 Go 프로젝트 레이아웃
가장 널리 알려진 패턴은 golang-standards/project-layout이지만, 이는 공식 표준이 아님을 유의해야 합니다. 일반적인 구조는 다음과 같습니다:
myproject/
├── cmd/
│ ├── api/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/
│ ├── auth/
│ ├── storage/
│ └── transport/
├── pkg/
│ ├── logger/
│ └── crypto/
├── api/
│ └── openapi.yaml
├── config/
│ └── config.yaml
├── scripts/
│ └── deploy.sh
├── go.mod
├── go.sum
└── README.md
많은 팀은 cmd/의 바이너리가 시작 시 하나의 로거를 연결할 수 있도록 internal/ 아래에 작은 공유 로깅 패키지를 유지합니다(예: internal/logx). 위의 스케치에서 pkg/logger/는 해당 코드가 다른 모듈에서 가져올 수 있을 때만 적절합니다. 프로덕션 지향적인 log/slog 설정(JSON 라인, 적색 처리, 컨텍스트 및 추적 필드, 모니터링 스택과 잘 작동하는 신호)에 대해서는 slog를 사용한 Go의 구조화된 로깅을 통한 관찰 가능성 및 알림을 참조하십시오.
cmd/ 디렉토리
cmd/ 디렉토리에는 애플리케이션 진입점이 있습니다. 각 하위 디렉토리는 별도의 실행 파일 바이너리를 나타냅니다. 예를 들어, cmd/api/main.go는 API 서버를 빌드하고, cmd/worker/main.go는 백그라운드 작업 프로세서를 빌드할 수 있습니다.
모범 사례: main.go 파일을 최소한으로 유지하십시오. 종속성을 연결하고, 구성을 로드하고, 애플리케이션을 시작하는 데 필요한 것만 포함하십시오. 모든 본질적인 로직은 main.go가 가져오는 패키지에 속해야 합니다.
// cmd/api/main.go
package main
import (
"log"
"myproject/internal/server"
"myproject/internal/config"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal(err)
}
srv := server.New(cfg)
if err := srv.Start(); err != nil {
log.Fatal(err)
}
}
internal/ 디렉토리
여기에는 프라이빗 애플리케이션 코드가 존재합니다. Go 컴파일러가 internal/ 내의 패키지를 외부 프로젝트가 가져오는 것을 방지하므로, 다음과 같은 용도로 이상적입니다:
- 비즈니스 로직 및 도메인 모델
- 애플리케이션 서비스
- 내부 API 및 인터페이스
- 데이터베이스 리포지토리(올바른 ORM 선택을 위한 PostgreSQL용 Go ORM 비교)
- 인증 및 권한 부여 로직
internal/은 기술적 레이어가 아닌 기능 또는 도메인별로 구성하십시오. internal/handlers/, internal/services/, internal/repositories/ 대신 internal/user/, internal/order/, internal/payment/을 선호하십시오. 각 패키지는 핸들러, 서비스 및 리포지토리를 포함합니다.
복잡한 디렉토리 구조로 시작해야 할까요?
절대 아닙니다. 작은 도구나 프로토타입을 빌드하는 경우 다음으로 시작하십시오:
myproject/
├── main.go
├── go.mod
└── go.sum
프로젝트가 성장하고 논리적 그룹화가 식별되면 디렉토리를 도입하십시오. 데이터베이스 로직이 상당히 커질 때 db/ 패키지를 추가하거나 HTTP 핸들러가 증가할 때 api/ 패키지를 추가할 수 있습니다. 사전에 강제로 부과하기보다 구조가 자연스럽게 등장하도록 하십시오.
플랫 vs 중첩 구조: 균형 찾기
Go 프로젝트 구조에서 가장 흔한 실誤 중 하나는 과도한 중첩입니다. Go는 얕은 계층 구조를 선호합니다—일반적으로 한 두 단계 깊이입니다. 깊은 중첩은 인지 부하를 증가시키고 가져오기를 번거롭게 만듭니다.
가장 흔한 실誤는 무엇인가요?
디렉토리 과중첩: internal/services/user/handlers/http/v1/와 같은 구조를 피하십시오. 이는 불필요한 탐색 복잡성을 만듭니다. 대신 internal/user/handler.go를 사용하십시오.
일반적인 패키지 이름: utils, helpers, common, base와 같은 이름은 코드 냄새(code smell)입니다. 이들은 특정 기능을 전달하지 않으며 종종 관련 없는 코드의 덤프장이 됩니다. 설명적인 이름을 사용하십시오: validator, auth, storage, cache.
순환 의존성: 패키지 A가 패키지 B를 가져오고 B가 A를 가져올 때 순환 의존성이 발생합니다. 이는 Go의 컴파일 오류입니다. 이는 일반적으로 관심사의 분리가 부족함을 나타냅니다. 인터페이스를 도입하거나 공유 타입을 별도의 패키지로 추출하십시오.
관심사 혼합: HTTP 핸들러는 HTTP 관심사에 초점을 맞추고, 데이터베이스 리포지토리는 데이터 액세스에 초점을 맞추며, 비즈니스 로직은 서비스 패키지에 유지하십시오. 핸들러에 비즈니스 규칙을 배치하면 테스트가 어려워지고 도메인 로직이 HTTP와 결합됩니다.
도메인 주도 설계 및 헥사고날 아키텍처
더 큰 애플리케이션, 특히 마이크로서비스의 경우, 도메인 주도 설계(DDD)와 헥사고날 아키텍처는 명확한 관심사 분리를 제공합니다.
도메인 주도 설계를 따르는 마이크로서비스를 어떻게 구조화하나요?
헥사고날 아키텍처는 의존성이 안쪽으로 흐르는 집중적인 레이어로 코드를 구성합니다:
internal/
├── domain/
│ └── user/
│ ├── entity.go # 도메인 모델 및 값 객체
│ ├── repository.go # 리포지토리 인터페이스 (포트)
│ └── service.go # 도메인 서비스
├── application/
│ └── user/
│ ├── create_user.go # 사용 사례: 사용자 생성
│ ├── get_user.go # 사용 사례: 사용자 조회
│ └── service.go # 애플리케이션 서비스 오케스트레이션
├── adapter/
│ ├── http/
│ │ └── user_handler.go # HTTP 어댑터 (REST 엔드포인트)
│ ├── postgres/
│ │ └── user_repo.go # 데이터베이스 어댑터 (리포지토리 포트 구현)
│ └── redis/
│ └── cache.go # 캐시 어댑터
└── api/
└── http/
└── router.go # 라우트 구성 및 미들웨어
도메인 레이어 (domain/): 핵심 비즈니스 로직, 엔티티, 값 객체 및 도메인 서비스 인터페이스. 이 레이어는 외부 시스템에 대한 의존성이 없습니다—HTTP나 데이터베이스 가져오기가 없습니다. 어댑터가 구현할 리포지토리 인터페이스(포트)를 정의합니다.
애플리케이션 레이어 (application/): 도메인 객체를 오케스트레이션하는 사용 사례. 각 사용 사례(예: “사용자 생성”, “결제 처리”)는 별도의 파일 또는 패키지입니다. 이 레이어는 도메인 객체를 조정하지만 자체적으로 비즈니스 규칙을 포함하지 않습니다.
어댑터 레이어 (adapter/): 내부 레이어에서 정의한 인터페이스를 구현합니다. HTTP 핸들러는 요청을 도메인 객체로 변환하고, 데이터베이스 리포지토리는 영속성을 구현하며, 메시지 큐는 비동기 통신을 처리합니다. 이 레이어에는 모든 프레임워크별 및 인프라 코드가 포함됩니다.
API 레이어 (api/): 라우트, 미들웨어, DTO(데이터 전송 객체), API 버전 관리 및 OpenAPI 사양.
이 구조는 핵심 비즈니스 로직이 테스트 가능하고 프레임워크, 데이터베이스 또는 외부 서비스와 독립적으로 유지되도록 보장합니다. 도메인 코드를 건드리지 않고 PostgreSQL을 MongoDB로, REST를 gRPC로 교체할 수 있습니다. 이 레이아웃 내에서 CQRS를 채택하면 Go에서 CQRS 구현이 application/ 레이어가 명령 핸들러 패키지와 조회 핸들러 패키지로 자연스럽게 매핑되는 방법을 보여주며, 명령 측을 엄격하게 하고 조회 측을 DTO 중심적으로 유지합니다.
다양한 프로젝트 유형을 위한 실용적인 패턴
작은 CLI 도구
명령줄 애플리케이션의 경우, 여러 명령 및 하위 명령을 지원하는 구조가 필요합니다. 명령 구조에는 Cobra, 구성 관리에는 Viper와 같은 프레임워크를 사용하는 것을 고려하십시오. Cobra & Viper를 사용한 Go CLI 애플리케이션 빌딩 가이드에서 이 패턴을 자세히 다룹니다.
mytool/
├── main.go
├── command/
│ ├── root.go
│ └── version.go
├── go.mod
└── README.md
REST API 서비스
Go에서 REST API를 빌드할 때, 이 구조는 관심사를 명확히 분리합니다: 핸들러는 HTTP 관심사를 처리하고, 서비스는 비즈니스 로직을 포함하며, 리포지토리는 데이터 액세스를 관리합니다. 표준 라이브러리 접근 방식, 프레임워크, 인증, 테스트 패턴 및 프로덕션 준비 모범 사례를 다루는 포괄적인 가이드를 위해 Go에서 REST API 빌딩을 위한 완전한 가이드를 참조하십시오. 구조화된 JSON 로깅, 요청 및 추적 상관관계, 알림을 지원하는 로그 형태에 대해서는 slog를 사용한 Go의 구조화된 로깅을 통한 관찰 가능성 및 알림을 참조하십시오.
myapi/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── config/
│ ├── middleware/
│ ├── user/
│ │ ├── handler.go
│ │ ├── service.go
│ │ └── repository.go
│ └── product/
│ ├── handler.go
│ ├── service.go
│ └── repository.go
├── pkg/
│ └── httputil/
├── go.mod
└── README.md
여러 서비스를 위한 모노레포
myproject/
├── cmd/
│ ├── api/
│ ├── worker/
│ └── scheduler/
├── internal/
│ ├── shared/ # 공유 내부 패키지
│ ├── api/ # API별 코드
│ ├── worker/ # Worker별 코드
│ └── scheduler/ # Scheduler별 코드
├── pkg/ # 공유 라이브러리
├── go.work # Go 워크스페이스 파일
└── README.md
테스트 및 문서화
_test.go 접미사를 사용하여 테스트 파일은 테스트하는 코드 옆에 배치하십시오:
internal/
└── user/
├── service.go
├── service_test.go
├── repository.go
└── repository_test.go
이 관례는 테스트를 구현 근처에 유지하여 찾고 유지하기 쉽게 만듭니다. 여러 패키지를 건드리는 통합 테스트의 경우, 프로젝트 루트에 별도의 test/ 디렉토리를 만드십시오. 테이블 기반 테스트, 목(mock), 커버리지 분석 및 모범 사례를 포함한 효과적인 단위 테스트 작성에 대한 포괄적인 지침은 Go 단위 테스트 구조 및 모범 사례 가이드를 참조하십시오.
문서는 다음에 속합니다:
README.md: 프로젝트 개요, 설정 지침, 기본 사용법docs/: 상세 문서, 아키텍처 결정, API 참조api/: OpenAPI/Swagger 사양, protobuf 정의
REST API의 경우, Swagger와 함께 OpenAPI 문서를 생성하고 서빙하는 것은 API 발견 가능성 및 개발자 경험에 필수적입니다. Go API에 Swagger 추가 가이드에서 인기 있는 프레임워크와의 통합 및 모범 사례를 다룹니다.
Go Modules로 종속성 관리
모든 Go 프로젝트는 종속성 관리를 위해 Go Modules를 사용해야 합니다. Go 명령 및 모듈 관리에 대한 포괄적인 참조는 Go 치트시트를 확인하십시오. 다음으로 초기화하십시오:
go mod init github.com/yourusername/myproject
이 명령은 go.mod(종속성 및 버전) 및 go.sum(검증용 체크섬)을 생성합니다. 재현 가능한 빌드를 위해 이러한 파일을 버전 관리에 유지하십시오.
종속성을 정기적으로 업데이트하십시오:
go get -u ./... # 모든 종속성 업데이트
go mod tidy # 사용하지 않는 종속성 제거
go mod verify # 체크섬 검증
주요 요약
-
단순하게 시작하고 자연스럽게 진화하십시오: 초기 구조를 과잉 설계하지 마십시오. 복잡성이 요구될 때 디렉토리와 패키지를 추가하십시오.
-
플랫 계층 구조를 선호하십시오: 중첩을 한 두 단계로 제한하십시오. Go의 플랫 패키지 구조는 가독성을 향상시킵니다.
-
설명적인 패키지 이름을 사용하십시오:
utils와 같은 일반적인 이름을 피하십시오. 패키지는 그들이 하는 일로 이름 짓습니다:auth,storage,validator. -
관심사를 명확히 분리하십시오: 핸들러는 HTTP에, 리포지토리는 데이터 액세스에, 비즈니스 로직은 서비스 패키지에 초점을 맞추십시오.
-
프라이버시를 위해
internal/을 활용하십시오: 외부에서 가져서는 안 되는 코드에 사용하십시오. 대부분의 애플리케이션 코드는 여기에 속합니다. -
필요할 때 아키텍처 패턴을 적용하십시오: 복잡한 시스템의 경우, 헥스코날 아키텍처와 DDD는 명확한 경계와 테스트 가능성을 제공합니다.
-
Go가 인도하도록 하십시오: 다른 언어에서 패턴을 가져오기보다 Go 관용구를 따르십시오. Go에는 단순성과 조직화에 대한 자체 철학이 있습니다.
유용한 링크
- Go 치트시트
- Go에서 REST API 빌딩: 완전한 가이드
- Cobra & Viper를 사용한 Go CLI 애플리케이션 빌딩
- Go 단위 테스트: 구조 및 모범 사례
- slog를 사용한 Go의 구조화된 로깅을 통한 관찰 가능성 및 알림
- Go API에 Swagger 추가
- Go 제네릭: 사용 사례 및 패턴
- PostgreSQL용 Go ORM 비교: GORM vs Ent vs Bun vs sqlc
기타 관련 기사
- Standard Go Project Layout - 커뮤니티 기반 프로젝트 구조 가이드라인
- Go Modules Reference - 공식 Go 모듈 문서
- Hexagonal Architecture in Go - 엔터프라이즈급 헥스코날 아키텍처 프레임워크
- Domain-Driven Design in Go - 프로덕션 준비 DDD 구현
- Go Project Structure Conventions - 추가 패턴 및 예제
- Effective Go - 공식 Go 모범 사례 가이드
- App Architecture hub — API design, code structure, and integration patterns