Go 프로젝트의 자기주장적인 구조
Emily Parker
Product Engineer · Leapcell

소개
활기 넘치는 Go 생태계에서 프로젝트 구조에 대한 주제는 종종 활발한 토론을 불러일으킵니다. 많은 신규 개발자 및 숙련된 개발자에게 "표준 Go 프로젝트 레이아웃" 리포지토리는 사실상의 참조 지점이 되었습니다. 이 리포지토리는 크고 복잡한 Go 프로젝트를 위한 공통 기반을 제공하는 것을 목표로 포괄적인 디렉토리 구조를 규정합니다. 이 이니셔티브는 모범 사례를 표준화하려는 훌륭한 시도이지만, 실제로는 대다수의 웹 애플리케이션에 대해 과도하거나 심지어 역효과를 낼 수 있는 청사진을 제시합니다. 이 글에서는 이 "표준" 레이아웃을 맹목적으로 채택하는 것이 Go가 지지하는 단순함과 민첩성을 어떻게 해칠 수 있는지, 특히 웹 서비스를 구축할 때를 중심으로 살펴봅니다. 대신, 웹 개발의 반복적인 특성과 일반적인 규모에 더 잘 맞는 대안적인 접근 방식을 탐색할 것입니다.
Go 프로젝트 구조 살펴보기
더 깊이 들어가기 전에, 논의할 핵심 개념에 대한 공통된 이해를 확립합시다.
"표준 Go 프로젝트 레이아웃"
이 널리 인용되는 리포지토리는 매우 자기주장적이고 계층적인 구조를 제안합니다. 주요 목표는 프로젝트를 별도의 책임으로 구성하는 것입니다:
cmd/: 프로젝트의 메인 애플리케이션을 포함합니다.cmd아래의 각 디렉토리는 별도의 실행 파일입니다.pkg/: 다른 프로젝트에서 일반적인 용도로 사용하기 위한 라이브러리 코드입니다.internal/: 다른 프로젝트에서 가져올 수 없는 비공개 애플리케이션 및 라이브러리 코드입니다.api/: API 정의 (예: OpenAPI, Protobuf).web/: 웹 애플리케이션 특정 구성 요소 (예: 정적 자산, 템플릿).configs/: 구성 파일 템플릿 또는 기본 구성입니다.build/: 패키징 및 연속 통합.deployments/: IaaS, PaaS 및 컨테이너 오케스트레이션 배포 구성입니다.scripts/: 다양한 빌드, 설치, 분석 또는 기타 관리 작업을 수행하는 스크립트입니다.test/: 외부 테스트 애플리케이션 및 추가 테스트 데이터입니다.
이 레이아웃의 근거는 복잡한 프로젝트, 특히 모노레포 또는 여러 개의 독립적인 실행 파일과 공유 내부 라이브러리를 가진 프로젝트에 맞게 확장 가능한 포괄적인 엔터프라이즈급 구조를 제공하는 것입니다.
웹 애플리케이션에 적합하지 않은 이유
많은 웹 애플리케이션, 특히 현대적인 마이크로서비스 또는 API 우선 패러다임을 따르는 애플리케이션은 더 집중되어 있습니다. 이들은 일반적으로 HTTP 요청 처리, 데이터베이스 상호 작용, 그리고 잠재적으로 몇 가지 외부 서비스와의 통신과 같이 단일 주요 목적을 수행합니다. "표준 Go 프로젝트 레이아웃"이 부담이 될 수 있는 특정 이유를 살펴보겠습니다.
1. 단순함을 위한 과도한 아키텍처링
"적을수록 많다"로 요약되는 Go의 철학은 간단하고 명시적인 디자인을 장려합니다. 복잡한 디렉토리 구조는 인지 부하를 추가합니다. 하나의 두 개의 메인 실행 파일 (cmd/api, cmd/worker)을 가진 일반적인 웹 API의 경우, cmd, pkg, internal의 분리는 강요되는 것처럼 느껴질 수 있습니다.
간단한 웹 서비스를 고려해 보세요:
// main.go package main import ( "log" "net/http" ) func helloHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, world!")) } func main() { http.HandleFunc("/", helloHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
이것을 cmd/api/main.go에 넣고, pkg와 internal 디렉토리가 비어 있거나 거의 채워지지 않았다면, 실제 코드에 비해 불균형적으로 느껴집니다. pkg와 internal의 가치는 같은 저장소 내의 여러 독립적인 애플리케이션에서 실제로 공유되는 재사용 가능한 구성 요소가 있거나 특정 패키지의 외부 가져오기를 명시적으로 방지하고 싶을 때 빛을 발합니다. 단일 웹 서비스의 경우, "internal" 구분은 주로 프로젝트 구조에 대한 간접 계층을 추가합니다.
2. internal 패키지 논쟁
internal 디렉토리는 "표준 레이아웃"의 핵심 원칙입니다. Go 컴파일러는 internal 디렉토리의 패키지는 internal이 거주하는 모듈 외부에서 코드를 가져올 수 없도록 강제합니다. 이것은 모듈성을 강제하는 강력한 기능입니다. 그러나 자체 포함된 웹 서비스의 경우, 거의 모든 애플리케이션 로직이 해당 서비스 내부에 있습니다. 모든 도메인 모델, 서비스 핸들러 및 리포지토리를 별도의 "internal" 하위 디렉토리에 배치하는 것은 중소 규모 프로젝트에 명확한 이점 없이 종종 코드베이스를 파편화합니다.
일반적인 웹 애플리케이션 구조를 예로 들어 설명해 보겠습니다:
표준 레이아웃 접근 방식:
my-webapp/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── auth/
│ │ └── service.go
│ ├── handlers/
│ │ └── user.go
│ ├── models/
│ │ └── user.go
│ └── repository/
│ └── user.go
└── go.mod
여기서 cmd/api/main.go는 internal/auth, internal/handlers 등을 가져옵니다. 기능적으로는 올바르지만, 단일 서비스 프로젝트에서 외부 가져오기를 방지해야 하는 필요성보다는 "internal" 접두사가 구성상의 관례로 더 많이 작용합니다.
대안 (더 평탄한) 접근 방식:
my-webapp/
├── main.go
├── auth/
│ └── service.go
├── handlers/
│ └── user.go
├── models/
│ └── user.go
├── repository/
│ └── user.go
└── go.mod
이 더 평탄한 구조에서는 모듈(auth, handlers, models, repository)이 프로젝트 루트 바로 아래에 있습니다. 다른 모듈에서 가져올 수 있는 것은 기술적으로 가능하지만 (이것이 라이브러리라면), 독립 실행형 애플리케이션의 경우 일반적으로 문제가 되지 않습니다. Go 커뮤니티는 종종 이를 "평탄한" 또는 "기능별 패키지" 레이아웃이라고 부릅니다. 인터페이스를 정의하고 구현 세부 정보를 숨기기 위해 Go의 강력한 패키지 경계를 활용하며 추가적인 "internal" 수준을 사용하지 않습니다.
3. 프로젝트 구성의 조기 최적화
초기부터 복잡한 레이아웃을 채택하는 것은 조기 최적화의 한 형태일 수 있습니다. 아직 프로젝트에 존재하지 않는 문제를 해결할 수 있습니다. 웹 애플리케이션이 발전함에 따라 코드를 리팩토링하고 재구성하는 것은 개발 프로세스의 자연스러운 부분입니다. 더 간단한 구조로 시작하면 유기적인 성장과 적응이 가능합니다.
예를 들어, 처음에 모든 HTTP 핸들러에 대한 handlers 패키지가 있다고 가정해 봅시다:
my-webapp/
├── main.go
├── handlers/
│ ├── user.go
│ └── product.go
└── go.mod
프로젝트가 커지고 user.go 및 product.go가 너무 커지면 기능별로 그룹화하기로 결정할 수 있습니다:
my-webapp/
├── main.go
├── user/
│ ├── handler.go
│ └── service.go
├── product/
│ ├── handler.go
│ └── service.go
└── go.mod
이러한 진화는 새로운 패키지가 pkg, internal 또는 새로운 cmd 항목 아래에 속할지 여부에 대해 끊임없이 논쟁할 필요가 없을 때 관리하기가 더 간단합니다.
4. "표준"은 권위를 의미합니다
"표준 Go 프로젝트 레이아웃"이라는 제목 자체는 상당한 비중을 가집니다. 신규 개발자는 이를 Go 프로젝트를 구성하는 유일하게 올바른 방법으로 인식하여 간단한 웹 서비스에 불필요한 복잡성을 초래할 수 있습니다. Go 팀 자체 (언어 제작자)는 공식적인 프로젝트 구조를 보증하지 않으며, 개별 프로젝트에 맡기는 것을 선호합니다. golang-standards 조직은 커뮤니티 주도이며 공식 Go 프로젝트가 아닙니다. 이 구분이 중요합니다.
웹 애플리케이션을 위한 더 실용적인 접근 방식
성공적인 많은 Go 웹 애플리케이션은 더 간단하고 실용적인 레이아웃을 선택합니다. 일반적인 패턴은 다음과 같습니다:
-
평탄한 구조 / 기능별 패키지: 관련 코드를 최상위 패키지에 그룹화하며, 종종 기능이나 도메인 개념의 이름을 따서 명명합니다.
my-webapp/ ├── main.go # 진입점 ├── config/ # 애플리케이션 구성 ├── server/ # HTTP 서버 설정, 미들웨어 ├── user/ # 사용자 관련 도메인, 서비스, 핸들러, 리포지토리 │ ├── handler.go │ ├── service.go │ └── repository.go ├── product/ # 제품 관련 도메인, 서비스, 핸들러, 리포지토리 │ ├── handler.go │ └── service.go ├── common/ # 유틸리티 함수, 공유 인터페이스 └── go.mod이 접근 방식은 기능에 대한 관련 코드를 함께 유지하여 검색 가능성과 응집력을 향상시킵니다.
main.go가 모든 것을 연결합니다. -
평탄한 구조 내의 계층형/육각형 (포트 및 어댑터): 더 엄격하게 관심사를 분리하려는 대규모 웹 애플리케이션의 경우, 더 평탄한 구조 내에서 계층형 아키텍처를 구현할 수 있습니다.
my-webapp/ ├── main.go ├── config/ ├── internal/ # 애플리케이션별 핵심 로직 (도메인, 사용 사례, 서비스) │ ├── domain/ │ │ ├── user.go │ │ └── product.go │ ├── service/ # 비즈니스 로직, 도메인 객체 오케스트레이션 │ │ ├── user.go │ │ └── product.go │ └── ports/ # 외부 종속성 (데이터베이스, HTTP, 메시지 큐)에 대한 인터페이스 │ ├── database.go │ └── http.go ├── adapters/ # 포트 구현 (예: 데이터베이스용 GORM, HTTP용 Gin/Echo) │ ├── http/ │ │ └── handler.go │ └── persistence/ │ └── postgres.go └── go.mod여기서
internal디렉토리는 핵심 애플리케이션 로직에 사용되며, 이는 직접적으로 외부에 노출되거나 외부 종속성을 가져서는 안 됩니다. 그런 다음adapters가 이 코어를 외부 세계와 연결합니다. 이는internal을 훨씬 더 구체적이고 의미 있게 사용하는 것입니다.
핵심은 현재 프로젝트의 규모와 복잡성에 가장 적합한 구조를 선택하여 자연스럽게 발전할 수 있도록 하는 것입니다. 간단하게 시작하고 실제로 직면하는 문제를 해결할 때만 복잡성을 도입하세요.
결론
"표준 Go 프로젝트 레이아웃"은 복잡하고 여러 실행 파일이 있는 프로젝트 또는 모노레포에 적합한 포괄적이면서도 자기주장적인 구조를 제공하지만, 일반적인 웹 애플리케이션에는 불필요한 복잡성을 야기하는 경우가 많습니다. Go의 강점은 단순함과 명시적인 디자인에 있으며, 프로젝트 구조는 이러한 가치를 반영해야 합니다. 대부분의 웹 서비스의 경우, 더 평탄하고 기능 지향적이거나 더 간결한 구조 내에서 잘 고려된 계층형 접근 방식은 명확성을 향상시키고, 인지 부하를 줄이며, 더 민첩한 개발을 촉진할 것입니다.
프로젝트 구조는 당신을 위해 존재해야 하며, 그 반대가 되어서는 안 됩니다.

