Go 프로젝트 구조: 실천 방법과 패턴

확장성과 명확성을 위해 Go 프로젝트를 구조화하세요

Page content

Go 프로젝트의 구조를 효과적으로 구성하는 것은 장기적인 유지보수, 팀 협업, 확장성에 기초적인 역할을 합니다. 프레임워크가 엄격한 디렉토리 레이아웃을 강제하는 것과 달리 Go는 유연성을 존중하지만, 이 자유는 프로젝트의 특정 요구사항에 맞는 패턴을 선택하는 책임을 가져옵니다.

project tree

Go의 프로젝트 구조에 대한 철학 이해

Go의 최소주의 설계 철학은 프로젝트 조직에도 적용됩니다. 이 언어는 특정한 구조를 강제하지 않고, 개발자가 정보를 바탕으로 결정하도록 신뢰합니다. 이 접근법은 커뮤니티가 여러 검증된 패턴을 개발하게 했으며, 작은 프로젝트를 위한 간단한 평면 레이아웃에서 기업 시스템을 위한 복잡한 아키텍처에 이르기까지 다양합니다.

핵심 원칙은 단순성 우선, 필요할 때 복잡성입니다. 많은 개발자가 초기 구조를 과도하게 설계하는 함정에 빠지며, 깊게 중첩된 디렉토리와 조기 추상화를 생성합니다. 오늘날 필요한 내용으로부터 시작하고, 프로젝트가 성장하면서 리팩토링하세요.

internal/ 디렉토리와 pkg/ 디렉토리를 언제 사용해야 하나요?

internal/ 디렉토리는 Go 설계에서 특정한 목적을 가지고 있습니다. 외부 프로젝트가 이 패키지를 임포트할 수 없도록 합니다. Go 컴파일러는 이 제한을 강제하며, internal/은 외부에서 재사용되지 않아야 하는 프라이빗 애플리케이션 논리, 비즈니스 규칙, 유틸리티에 이상적입니다.

반면, pkg/ 디렉토리는 외부에서 사용될 코드를 나타냅니다. 외부에서 사용할 라이브러리 또는 재사용 가능한 구성 요소를 만들고자 한다면 여기에만 코드를 배치하세요. 많은 프로젝트는 pkg/가 전혀 필요하지 않을 수 있습니다. 외부에서 코드가 사용되지 않는다면, 루트 또는 internal/에 두는 것이 더 깔끔합니다. 재사용 가능한 라이브러리를 만들 때는 Go generics를 활용하여 타입 안전하고 재사용 가능한 구성 요소를 생성하세요.

표준 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/ 디렉토리

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 ORMs 비교를 참조하세요.)
  • 인증 및 인가 논리

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는 얕은 계층 구조를 선호합니다. 일반적으로 1~2단계 깊이입니다. 깊은 중첩은 인지 부하를 증가시키고 임포트를 복잡하게 만듭니다.

가장 흔한 실수는 무엇인가요?

디렉토리의 과도한 중첩: internal/services/user/handlers/http/v1/와 같은 구조를 피하세요. 이는 불필요한 탐색 복잡성을 생성합니다. 대신 internal/user/handler.go를 사용하세요.

일반적인 패키지 이름: utils, helpers, common, base와 같은 이름은 코드 냄새입니다. 특정 기능을 전달하지 않으며 종종 관련 없는 코드를 덤프하는 곳이 됩니다. 설명적인 이름을 사용하세요: validator, auth, storage, cache.

순환 의존성: 패키지 A가 패키지 B를 임포트하고 B가 A를 임포트하면 순환 의존성이 발생합니다. 이는 Go에서 컴파일 오류입니다. 이는 일반적으로 관심사의 분리가 부족한 신호입니다. 인터페이스를 도입하거나 공유된 타입을 별도의 패키지로 추출하세요.

관심사 혼합: HTTP 핸들러는 HTTP 관련 관심사에 집중하고, 데이터베이스 저장소는 데이터 접근에 집중하며, 비즈니스 논리는 서비스 패키지에 포함되어야 합니다. 핸들러에 비즈니스 규칙을 배치하면 테스트가 어렵고 도메인 논리를 HTTP에 결합하게 됩니다.

도메인 주도 설계와 헥사곤 아키텍처

더 큰 애플리케이션, 특히 마이크로서비스에서 도메인 주도 설계(Domain-Driven Design, 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로 교체할 수 있고, 도메인 코드를 변경하지 않아도 됩니다.

다양한 프로젝트 유형을 위한 실용 패턴

작은 CLI 도구

명령행 애플리케이션을 만들 때는 여러 명령 및 하위 명령을 지원하는 구조가 필요합니다. Cobra를 명령 구조에, Viper를 설정 관리에 사용하는 것이 좋습니다. 우리의 Go에서 Cobra 및 Viper를 사용한 CLI 애플리케이션 개발는 이 패턴을 상세히 다룹니다.

mytool/
├── main.go
├── command/
│   ├── root.go
│   └── version.go
├── go.mod
└── README.md

REST API 서비스

Go에서 REST API를 구축할 때, 핸들러는 HTTP 관련 관심사를 처리하고, 서비스는 비즈니스 논리를 포함하며, 저장소는 데이터 접근을 관리합니다. 표준 라이브러리 접근법, 프레임워크, 인증, 테스트 패턴, 프로덕션 준비 최고 실천 방법을 포함한 완전한 가이드는 우리의 Go에서 REST API 구현 가이드를 참조하세요.

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/       # 워커 관련 코드
│   └── scheduler/    # 스케줄러 관련 코드
├── pkg/              # 공유 라이브러리
├── go.work           # Go 워크스페이스 파일
└── README.md

테스트 및 문서화

테스트 파일은 _test.go 접미사를 사용하여 테스트하는 코드와 함께 배치하세요:

internal/
└── user/
    ├── service.go
    ├── service_test.go
    ├── repository.go
    └── repository_test.go

이 관행은 테스트를 구현에 가까이 두어 쉽게 찾고 유지보수가 가능하게 합니다. 여러 패키지를 사용하는 통합 테스트를 위해 프로젝트 루트에 별도의 test/ 디렉토리를 생성하세요. 효과적인 단위 테스트를 작성하는 방법, 테이블 기반 테스트, 모킹, 커버리지 분석, 최고의 실천 방법에 대한 종합적인 지침은 우리의 Go 단위 테스트 구조 및 최고의 실천 방법를 참조하세요.

문서화는 다음과 같은 위치에 포함하세요:

  • README.md: 프로젝트 개요, 설정 지침, 기본 사용법
  • docs/: 상세한 문서, 아키텍처 결정, API 참조
  • api/: OpenAPI/Swagger 명세, protobuf 정의

REST API의 경우, Swagger를 사용하여 OpenAPI 문서를 생성하고 제공하는 것이 API의 발견성과 개발자 경험에 필수적입니다. 우리의 Go API에 Swagger 추가 가이드는 인기 있는 프레임워크와 최고의 실천 방법을 다룹니다.

Go 모듈을 사용한 의존성 관리

모든 Go 프로젝트는 의존성 관리를 위해 Go 모듈을 사용해야 합니다. Go 명령어 및 모듈 관리에 대한 종합적인 참조는 우리의 Go 쉬트메이트를 참조하세요. 다음과 같이 초기화하세요:

go mod init github.com/yourusername/myproject

이 명령은 go.mod (의존성 및 버전)와 go.sum (검증을 위한 해시)를 생성합니다. 이 파일들은 재현 가능한 빌드를 위해 버전 제어에 포함되어야 합니다.

의존성을 정기적으로 업데이트하세요:

go get -u ./...          # 모든 의존성을 업데이트
go mod tidy              # 사용되지 않는 의존성을 제거
go mod verify            # 해시를 검증

핵심 요약

  1. 간단하게 시작, 자연스럽게 진화: 초기 구조를 과도하게 설계하지 마세요. 복잡성이 요구할 때 디렉토리와 패키지를 추가하세요.

  2. 평면 계층 구조를 선호하세요: 중첩을 1~2단계로 제한하세요. Go의 평면 패키지 구조는 가독성을 향상시킵니다.

  3. 설명적인 패키지 이름을 사용하세요: utils와 같은 일반적인 이름을 피하세요. 패키지가 무엇을 수행하는지 명시적으로 이름지어주세요: auth, storage, validator.

  4. 관심사를 명확하게 분리하세요: 핸들러는 HTTP에 집중하고, 저장소는 데이터 접근에 집중하며, 비즈니스 논리는 서비스 패키지에 포함되어야 합니다.

  5. internal/을 사용하여 프라이버시를 보호하세요: 외부에서 임포트되어서는 안 되는 코드를 여기에 배치하세요. 대부분의 애플리케이션 코드는 여기에 포함되어야 합니다.

  6. 필요할 때 아키텍처 패턴을 적용하세요: 복잡한 시스템에서는 헥사곤 아키텍처와 DDD가 명확한 경계와 테스트 가능성을 제공합니다.

  7. Go의 철학을 따르세요: 다른 언어의 패턴을 가져오지 말고, Go의 단순성과 조직 철학을 따르세요.

유용한 링크

관련 기사