Go 패키지 및 모듈 구조 설계 – 업무에 사용하는 Go 언어 응용편 11

Go 패키지 및 모듈 구조 설계
Go 패키지 및 모듈 구조 설계

Go 언어의 기본 문법을 익혔다면, 이제 실제 프로젝트에서 가장 중요한 부분 중 하나인 Go 패키지와 모듈 구조 설계에 대해 알아보겠습니다. 올바른 구조 설계는 코드의 가독성과 유지보수성을 크게 좌우하며, 특히 팀 단위로 개발할 때 그 중요성이 더욱 부각됩니다.

Go 패키지의 기본 개념과 설계 원칙

go 패키지는 Go 언어에서 코드를 구조화하고 재사용성을 높이는 핵심 요소입니다. 하나의 디렉토리 안에 있는 Go 파일들은 동일한 패키지에 속하게 되며, 이를 통해 관련된 기능들을 논리적으로 그룹화할 수 있습니다.

패키지 설계 시 가장 중요한 원칙은 단일 책임 원칙(Single Responsibility Principle)입니다. 각 패키지는 명확하고 구체적인 하나의 목적을 가져야 하며, 패키지명만 보더라도 그 역할을 쉽게 파악할 수 있어야 합니다.

Go
// 잘못된 예: 너무 광범위한 패키지
package utils

// 올바른 예: 구체적인 목적의 패키지
package fileparser
package httphandler
package validation

위 예시에서 보듯이 utils와 같은 광범위한 네이밍보다는 구체적인 기능을 나타내는 패키지명을 사용하는 것이 바람직합니다. 이렇게 하면 다른 개발자들이 코드를 이해하기 쉬워지고, 패키지 간의 의존성도 명확해집니다.

go mod를 활용한 모듈 관리 체계

Go 1.11부터 도입된 go mod는 의존성 관리를 위한 공식 도구입니다. 기존의 GOPATH 방식과는 달리, 모듈 단위로 버전 관리가 가능하며 재현 가능한 빌드를 보장합니다.

Go
// go.mod 파일 예시
module example.com/myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    gorm.io/gorm v1.25.4
)

replace github.com/old/package => github.com/new/package v1.0.0

모듈 초기화는 go mod init 명령어로 수행하며, 이때 모듈명은 일반적으로 저장소 URL을 사용합니다. require 섹션에는 프로젝트에서 사용하는 외부 의존성들이 명시되고, replace 섹션을 통해 특정 패키지를 다른 버전이나 로컬 경로로 대체할 수 있습니다.

필자는 go언어를 활용하여 웹 사이트를 구성한 경험이 있으며, 초기에는 go mod의 중요성을 간과했다가 프로젝트 규모가 커지면서 의존성 충돌 문제를 겪었던 기억이 있습니다. 이후 체계적인 모듈 관리의 필요성을 절감하게 되었습니다.

실무에서의 디렉토리 구조 설계

대규모 Go 프로젝트에서는 일관된 디렉토리 구조가 필수입니다. 다음은 일반적으로 권장되는 프로젝트 구조입니다.

Go
myproject/
├── cmd/
│   ├── server/
│   │   └── main.go
│   └── cli/
│       └── main.go
├── internal/
│   ├── handlers/
│   ├── services/
│   └── models/
├── pkg/
│   ├── logger/
│   └── config/
├── api/
├── web/
├── configs/
├── scripts/
├── test/
├── docs/
├── go.mod
└── go.sum

이 구조에서 cmd/ 디렉토리는 실행 가능한 애플리케이션들을 포함하고, internal/ 디렉토리는 해당 프로젝트에서만 사용되는 코드들을 담습니다. pkg/ 디렉토리는 다른 프로젝트에서도 재사용 가능한 라이브러리 코드들을 포함합니다.

특히 internal/ 디렉토리는 Go 언어의 특별한 의미를 가지는데, 이 안의 go 패키지들은 외부에서 직접 import할 수 없도록 보호됩니다. 이를 통해 API의 안정성을 보장하고 내부 구현 세부사항을 숨길 수 있습니다.

패키지 간 의존성 관리와 순환 참조 방지

Go에서는 패키지 간 순환 참조가 컴파일 에러를 발생시키므로, 설계 단계에서부터 의존성 방향을 명확히 해야 합니다. 일반적으로 다음과 같은 계층 구조를 따릅니다.

handlers (웹 핸들러)
    ↓
services (비즈니스 로직)
    ↓
repositories (데이터 접근)
    ↓
models (데이터 구조)

필자는 추후에 gin 웹 프레임워크를 통해 고도화된 사이트를 만들었는데, 초기 설계에서 의존성 방향을 잘못 설정하여 리팩토링에 상당한 시간을 투자했던 경험이 있습니다. 특히 models 패키지에서 handlers 패키지를 참조하는 실수를 범했을 때, 순환 참조 문제로 인해 전체 구조를 재설계해야 했습니다.

인터페이스 활용을 통한 유연한 설계

Go의 인터페이스는 패키지 간 결합도를 낮추고 테스트 가능성을 높이는 핵심 도구입니다. 다음 예시를 살펴보겠습니다:

Go
// internal/services/user.go
package services

import "context"

type UserRepository interface {
    GetUser(ctx context.Context, id string) (*User, error)
    CreateUser(ctx context.Context, user *User) error
}

type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) GetUserProfile(ctx context.Context, id string) (*UserProfile, error) {
    user, err := s.repo.GetUser(ctx, id)
    if err != nil {
        return nil, err
    }
    
    // 비즈니스 로직 처리
    profile := &UserProfile{
        ID:    user.ID,
        Name:  user.Name,
        Email: user.Email,
    }
    
    return profile, nil
}

위 코드에서 UserService는 구체적인 repository 구현체에 의존하지 않고 UserRepository 인터페이스에만 의존합니다. 이를 통해 다양한 저장소 구현체(메모리, 데이터베이스, 외부 API 등)를 유연하게 교체할 수 있으며, 단위 테스트에서는 모킹된 구현체를 사용할 수 있습니다.

NewUserService 함수는 의존성 주입을 위한 생성자 역할을 하며, 이러한 패턴을 통해 코드의 테스트 가능성과 유지보수성을 크게 향상시킬 수 있습니다.


정리

효과적인 go 패키지 및 모듈 구조 설계는 단순히 코드를 정리하는 것 이상의 의미를 가집니다. 올바른 구조는 팀 협업을 원활하게 하고, 코드의 재사용성을 높이며, 장기적인 유지보수 비용을 절감시켜 줍니다. go mod를 통한 체계적인 의존성 관리와 함께 인터페이스 기반의 유연한 설계를 적용한다면, 확장 가능하고 견고한 Go 애플리케이션을 구축할 수 있을 것입니다.