Go 언어 프로젝트의 패키지 구조는 일반적으로 몇 가지 컨벤션을 따르며, pkginternal 디렉토리는 그중 핵심적인 역할을 합니다. 단순히 코드를 기능별로 묶는 것을 넘어, 패키지의 가시성(visibility)과 의존성 방향을 명확히 하여 프로젝트의 유지보수성을 높이는 것이 목적입니다.

이 글은 Gemini 2.5 Pro 모델이 제 Go package convention 에 대한 질문에 답변한 내용입니다.


Go 커뮤니티에서 널리 통용되는 표준 프로젝트 레이아웃(golang-standards/project-layout)을 기반으로 각 디렉토리의 역할과 베스트 프랙티스를 자세히 설명해 드리겠습니다.

📦 pkg 디렉토리: 공개 라이브러리 (Public Library)#

pkg 디렉토리에는 외부 프로젝트에서 가져다 쓸 수 있는(importable) 공개 라이브러리 성격의 코드를 위치시킵니다.

  • 주요 특징:

    • 외부 공개용: 이 디렉토리 안의 패키지들은 다른 프로젝트에서 라이브러리처럼 import하여 사용하는 것을 염두에 둡니다.
    • 독립성: 이상적으로 pkg 내의 패키지는 해당 프로젝트의 다른 내부 패키지(특히 internal이나 cmd 등)에 대한 의존성이 없어야 합니다. 이는 라이브러리로서의 재사용성을 높이기 위함입니다.
    • 안정적인 API: 외부에 공개되는 만큼, 내부 로직보다 API의 안정성과 명확한 문서화가 중요합니다.
  • 예시:

    • pkg/user : 사용자 인증, 정보 관리 등 범용적인 사용자 관련 기능
    • pkg/billing : 결제 처리 로직
    • pkg/httpclient : 특정 서비스 API를 감싼(wrapping) 클라이언트

💡 “정말 pkg가 필요한가?” 사실 소규모 프로젝트나 라이브러리 자체가 목적인 프로젝트에서는 pkg 디렉토리가 굳이 필요 없을 수 있습니다. 프로젝트 루트에 바로 패키지를 두어도 외부에서 import가 가능하기 때문입니다. pkg“이 프로젝트가 제공하는 공개 API는 여기에 있습니다” 라는 것을 명확히 하고, 내부 로직과 공개 로직을 분리하고 싶을 때 유용합니다.

🔐 internal 디렉토리: 내부 전용 패키지 (Internal-Only)#

internal 디렉토리는 Go 컴파일러가 특별하게 취급하는 디렉토리입니다. 이 안에 있는 패키지들은 오직 해당 프로젝트 내에서만 import할 수 있습니다.

  • 주요 특징:

    • 외부 접근 차단: Go 컴파일러가 강제하는 규칙입니다. 예를 들어, myproject/internal/auth 패키지는 another-project에서 import하려고 하면 컴파일 에러가 발생합니다.
    • 내부 로직 캡슐화: 프로젝트의 핵심 비즈니스 로직, 데이터베이스 처리, 외부 서비스와의 연동 등 외부에 직접 노출하고 싶지 않은 코드들을 이곳에 둡니다.
    • 자유로운 리팩토링: 외부에서의 사용을 고려할 필요가 없으므로, 내부 구현을 훨씬 자유롭게 변경하고 리팩토링할 수 있습니다.
  • internal 하위 구조:

    • internal/app 또는 internal/server: HTTP 서버, gRPC 서버 등 애플리케이션의 진입점과 관련된 로직 (라우팅, 미들웨어 등)
    • internal/repository 또는 internal/db: 데이터베이스와의 상호작용을 담당하는 계층
    • internal/service 또는 internal/domain: 핵심 비즈니스 로직
    • internal/config: 설정 관리

💡 pkg vs internal pkg“가져다 쓰세요 (Public API)” 라면, internal“우리만 쓸 겁니다 (Private Implementation)” 라는 명확한 의도를 코드 구조로 표현하는 것입니다.

🚀 그 외 주요 디렉토리 컨벤션#

pkginternal 외에도 자주 사용되는 디렉토리 구조는 다음과 같습니다.

cmd#

애플리케이션의 진입점(main 함수)이 위치하는 곳 입니다. 보통 이 디렉토리 하위에 실행 파일 이름과 동일한 이름의 하위 디렉토리를 만들어 main.go 파일을 둡니다.

  • 역할: internalpkg에 정의된 패키지들을 조합하여 실제 애플리케이션을 실행시키는 역할을 합니다. 코드는 최소한으로 유지하고, 대부분의 로직은 다른 패키지에서 가져와 호출하는 형태가 이상적입니다.
  • 예시:
    • cmd/my-api-server/main.go
    • cmd/my-cli-tool/main.go

api#

API 명세 파일들이 위치합니다.

  • 예시:
    • OpenAPI (Swagger) *.yaml, *.json 파일
    • gRPC .proto 파일
    • GraphQL 스키마 파일

configs#

설정 파일 예시나 기본 템플릿이 위치합니다. 실제 운영 환경의 비밀 정보(secret)를 여기에 두어서는 안 됩니다.

  • 예시: config.example.yaml, dev.config.json

scripts#

빌드, 배포, 설치 등 프로젝트 관리에 필요한 스크립트들이 위치합니다.

  • 예시: build.sh, deploy.sh

test#

외부 테스트(E2E, 통합 테스트) 코드나 테스트 관련 데이터, 스크립트가 위치합니다. (단위 테스트는 각 패키지 내에 _test.go 파일로 작성하는 것이 일반적입니다.)

web 또는 ui#

웹 프론트엔드 관련 정적 파일(HTML, CSS, JS)이나 템플릿 파일이 위치합니다.

Best Practices 요약#

  1. 공개할 코드만 pkg에 두세요. 다른 프로젝트에서 재사용할 가치가 있고, 안정적인 API를 제공할 수 있는 코드만 pkg로 분리합니다.
  2. 모든 내부 로직은 internal에 두세요. 프로젝트의 핵심 로직, 외부에 노출할 필요 없는 구현 상세는 모두 internal 디렉토리 안에서 시작하는 것이 안전하고 좋습니다.
  3. cmd는 최대한 가볍게 유지하세요. main 함수는 각 패키지의 코드를 조립하고 실행하는 역할만 담당하게 하여, 로직이 main에 집중되지 않도록 합니다.
  4. 의존성 방향을 지키세요. 일반적으로 cmd -> internal -> pkg 순으로 의존성이 흐릅니다. 즉, pkg는 다른 내부 패키지를 몰라야 하고, internalcmd를 몰라야 합니다. 이 규칙은 프로젝트를 유연하고 테스트하기 쉽게 만듭니다.
  5. 처음부터 너무 복잡하게 시작하지 마세요. 작은 프로젝트라면 루트에 바로 패키지를 만들고, 프로젝트가 커지고 복잡해지면서 internal, pkg 등의 구조를 도입하는 것도 좋은 방법입니다.

이러한 구조는 강제적인 규칙은 아니지만, 많은 Go 개발자들이 따르는 “사실상의 표준(de facto standard)“입니다. 이 컨벤션을 따르면 다른 개발자가 프로젝트 구조를 훨씬 빠르게 이해하고 협업하는 데 큰 도움이 됩니다.