Go에서의 가시성 - 대문자 및 소문자 식별자 비하인드 스토리
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Go의 단순성은 놀라울 정도로 간단하고 우아한 가시성 규칙으로 확장됩니다. public
, private
또는 protected
와 같은 명시적 키워드에 의존하는 언어와 달리, Go는 식별자 명명에 깊이 뿌리내린 컨벤션을 활용합니다. 첫 글자의 대소문자가 바로 그것입니다. 이 사소해 보이는 디테일, 즉 식별자가 대문자 또는 소문자로 시작하는지는 가시성의 유일한 결정 요인입니다. 이 글에서는 이러한 규칙의 미묘한 차이를 자세히 살펴보고 이러한 규칙이 다양한 Go 구성 요소의 접근성을 어떻게 관리하는지 설명합니다.
핵심 원칙: 내보내기(Exported) vs. 내보내지 않음(Unexported)
본질적으로 Go는 식별자에 대해 두 가지 수준의 가시성을 정의합니다.
- 내보내기(Exported, Public): 대문자로 시작하는 식별자는 "내보내기"됩니다. 이는 선언된 패키지 외부에서 해당 식별자를 볼 수 있고 접근할 수 있음을 의미합니다.
- 내보내지 않음(Unexported, Private/Package-Local): 소문자로 시작하는 식별자는 "내보내지 않음"됩니다. 이는 선언된 패키지 내부에서만 해당 식별자를 볼 수 있고 접근할 수 있음을 의미합니다. 해당 패키지 외부에서는 접근할 수 없습니다.
이 규칙은 모든 최상위 선언에 보편적으로 적용됩니다.
- 변수
- 상수
- 함수
- 타입(구조체, 인터페이스 등)
- 구조체 필드
- 인터페이스 메서드
예시를 통해 이를 자세히 살펴보겠습니다.
패키지 수준 가시성
geometry
라는 이름의 패키지가 있다고 가정해 봅시다.
// geometry/shapes.go package geometry import "fmt" // Circle은 원을 나타내는 내보내진 구조체입니다. type Circle struct { Radius float64 // Radius는 내보내진 필드입니다. color string // color는 내보내지지 않은 필드입니다. } // area는 원의 면적을 계산합니다. 내보내지지 않았습니다. func area(r float64) float64 { return 3.14159 * r * r } // NewCircle은 내보내진 생성자 함수입니다. func NewCircle(radius float64, c string) *Circle { return &Circle{Radius: radius, color: c} } // GetArea는 원의 면적을 반환하는 내보내진 메서드입니다. func (c *Circle) GetArea() float64 { // 같은 패키지 내에 있기 때문에 private 필드 'color'와 내보내지지 않은 함수 'area'에 접근할 수 있습니다. fmt.Printf("Calculating area for a %s circle.\n", c.color) return area(c.Radius) } // privateConstant는 내보내지지 않은 상수입니다. const privateConstant = "This is private to the geometry package." // ExportedConstant는 내보내진 상수입니다. const ExportedConstant = "This is public to the geometry package." // privateVar는 내보내지지 않은 패키지 수준 변수입니다. var privateVar = 10 // PublicVar는 내보내진 패키지 수준 변수입니다. var PublicVar = 20
이제 main
과 같은 다른 패키지가 geometry
와 어떻게 상호 작용하는지 살펴봅시다.
// main.go package main import ( "fmt" "your_module/geometry" // your_module을 실제 모듈 경로로 바꾸세요. ) func main() { // 내보내진 타입, 함수 및 상수에 접근 c := geometry.NewCircle(5.0, "red") fmt.Println("Circle Radius:", c.Radius) // OK: Radius는 내보내졌습니다. fmt.Println("Circle Area:", c.GetArea()) // OK: GetArea는 내보내졌습니다. // fmt.Println("Circle Color:", c.color) // 컴파일 에러: c.color는 내보내지지 않았습니다. // fmt.Println(geometry.area(10)) // 컴파일 에러: area는 내보내지지 않았습니다. // fmt.Println(geometry.privateConstant) // 컴파일 에러: privateConstant는 내보내지지 않았습니다. // fmt.Println(geometry.privateVar) // 컴파일 에러: privateVar는 내보내지지 않았습니다. fmt.Println("Exported Constant:", geometry.ExportedConstant) // OK: ExportedConstant는 내보내졌습니다. fmt.Println("Public Variable:", geometry.PublicVar) // OK: PublicVar는 내보내졌습니다. // Circle을 직접 생성합니다. 내보내진 필드만 직접 설정할 수 있습니다. // Radius는 설정할 수 있지만 color는 설정할 수 없습니다. // color를 설정하려면 내보내진 메서드나 생성자를 사용해야 합니다. c2 := geometry.Circle{Radius: 7.0} // 내보내진 필드 설정은 OK // c3 := geometry.Circle{radius: 7.0} // 컴파일 에러: radius는 내보내지지 않았습니다 (필드 이름은 Radius이지만). // c4 := geometry.Circle{color: "blue"} // 컴파일 에러: color는 내보내지지 않았습니다. fmt.Println("Circle2 Radius:", c2.Radius) // 중요한 참고 사항: 다른 패키지에서 복합 리터럴을 사용하여 구조체를 직접 생성하는 경우에도 // 내보내진 필드만 초기화할 수 있습니다. // 리터럴이 다른 패키지에 있는 경우 복합 리터럴을 사용하여 내보내지지 않은 필드를 직접 설정할 수 없습니다. }
내보내지지 않은 식별자에 접근하려고 할 때 발생하는 오류 메시지는 다음과 유사할 것입니다.
c.color undefined (cannot refer to unexported field or method color)
geometry.area undefined (cannot refer to unexported name geometry.area)
왜 이런 디자인인가?
Go의 가시성 접근 방식은 여러 가지 이점을 제공합니다:
- 단순성과 가독성: 이 규칙은 기억하고 적용하기가 매우 간단합니다. 여러 키워드나 복잡한 접근 제어자를 배울 필요가 없습니다. 식별자를 보면 가시성을 알 수 있습니다.
- 명시적인 의도: 관례에 따라
Foo
를 작성하면 공개임을 명시적으로 나타내고foo
는 내부 사용을 의미합니다. 이는 좋은 API 디자인을 강화합니다. - 더 나은 디자인을 장려: 작성자가 코드의 어떤 부분을 API로 노출해야 하고 어떤 부분을 구현 세부 정보로 유지해야 하는지 생각하도록 미묘하게 장려합니다. 이는 본질적으로 더 나은 캡슐화와 모듈성을 초래합니다.
- 캡슐화 강제: 내보내지지 않은 요소는 패키지의 내부 구성 요소 역할을 하여 패키지 유지 관리자가 외부 사용자에게 영향을 주지 않고 구현을 리팩터링하거나 변경할 수 있습니다. 내보내진 API만 "계약"의 일부입니다.
특정 경우 및 모범 사례
구조체 필드
Circle
예제에서 볼 수 있듯이 struct
내의 개별 필드도 이러한 규칙을 따릅니다. 이를 통해 패키지 외부에서 데이터 구조의 어떤 부분이 접근 가능한지에 대한 세분화된 제어가 가능합니다. 종종 내보내지지 않은 필드는 내보내진 메서드를 통해 관리되는 내부 상태에 사용됩니다 (예: color
초기화를 위한 NewCircle
, 사용을 위한 GetArea
).
인터페이스 메서드
구조체 필드와 유사하게 인터페이스 내에 선언된 메서드도 가시성 규칙을 따릅니다. 인터페이스 메서드가 대문자로 시작하면 내보내진 메서드이며, 해당 인터페이스를 구현하는 모든 타입은 해당 시그니처를 가진 내보내진 메서드를 제공해야 함을 의미합니다.
// geometry/shapes.go (계속) package geometry // Shape는 내보내진 인터페이스입니다. type Shape interface { Area() float64 // 내보내진 메서드 perimeter() float64 // 내보내지지 않은 메서드 - 이는 가능하지만, 외부 사용을 의도한 인터페이스의 경우 덜 일반적입니다. # 이 인터페이스를 구현하는 외부 타입도 내보내지지 않은 'perimeter' 메서드를 제공해야 하며, 이는 해당 타입이 *같은* 패키지에 있는 경우에만 가능합니다. # 이는 내부 인터페이스에 더 자주 사용됩니다. } // Square는 Shape 인터페이스를 구현합니다 (가상). type Square struct { side float64 } func (s *Square) Area() float64 { // Shape의 내보내진 Area()를 만족시키려면 내보내져야 합니다. return s.side * s.side } func (s *Square) perimeter() float64 { // Shape의 내보내지지 않은 perimeter()를 만족시키려면 내보내지지 않아야 합니다. return 4 * s.side }
생성자 함수
내보내지지 않은 구조체 타입을 가지며, 이러한 타입의 인스턴스를 생성하는 "생성자" 역할을 하는 하나 이상의 내보내진 함수를 갖는 것이 일반적인 Go 관례입니다. 이를 통해 패키지는 타입이 인스턴스화되고 초기화되는 방식을 엄격하게 제어할 수 있습니다.
// internal/user.go package internal // user는 사용자를 나타내는 내보내지지 않은 구조체입니다. type user struct { id string name string } // NewUser는 'user' 타입에 대한 내보내진 생성자 함수입니다. func NewUser(id, name string) *user { return &user{id: id, name: name} } // GetName은 사용자 이름을 액세스하기 위한 내보내진 메서드입니다. func (u *user) GetName() string { return u.name } // privateMethod는 내보내지지 않았습니다. func (u *user) privateMethod() { // ... 내부 작업 수행 ... }
// main.go package main import ( "fmt" "your_module/internal" ) func main() { // u := internal.user{id: "123", name: "Alice"} // 컴파일 에러: user는 내보내지지 않았습니다. u := internal.NewUser("456", "Bob") // OK: NewUser는 내보내졌습니다. fmt.Println("User Name:", u.GetName()) // OK: GetName은 내보내졌습니다. // fmt.Println("User ID:", u.id) // 컴파일 에러: u.id는 내보내지지 않았습니다 (u 자체가 내보내지지 않은 타입의 포인터일지라도). // u.privateMethod() // 컴파일 에러: privateMethod는 내보내지지 않았습니다. }
여기서 internal.user
는 main
패키지에서 완전히 숨겨져 있습니다. main
패키지는 내보내진 NewUser
함수와 GetName
메서드를 통해서만 상호 작용할 수 있습니다. 이는 강력한 캡슐화를 제공합니다.
명명 규칙 및 컨텍스트
대소문자 규칙은 엄격하지만, "public" 또는 "private"의 의미는 항상 패키지 경계에 상대적입니다. tempCount
변수는 package metrics
내에서 내보내지지 않아 외부에서 보이지 않을 수 있습니다. 그러나 metrics.GetMetricCount()
라는 내보내진 함수가 그 값을 반환하는 경우, 개념적으로 해당 값은 함수를 통해 "public"이 됩니다. 이것은 Go의 가시성 설계가 원시 데이터 접근이 아닌 API 표면을 안내한다는 것을 더욱 강조합니다.
결론
첫 글자의 대소문자(대문자는 내보내기, 소문자는 내보내지 않음)만으로 결정되는 Go의 가시성 규칙은 Go 디자인 철학의 초석입니다. 즉, 단순성, 명확성, 구성보다 컨벤션을 따르는 것입니다. 이 우아한 메커니즘은 캡슐화를 기본값으로 만들고 API 노출을 위한 명시적인 의도를 요구함으로써 좋은 소프트웨어 아키텍처를 촉진합니다. 이러한 규칙을 이해하고 준수하는 것은 관용적이고 유지 관리 가능하며 강력한 Go 애플리케이션을 작성하는 데 기본이며, 패키지 내부를 격리된 상태로 유지하면서 안정적인 API를 명확하게 정의합니다.