패키지, 임포트에 대한 심화 학습

규칙을 따르지 않으면 손발이 고생한다.

이제 겨우 타입 정도를 다룬 마당에 굉장히 이른 감은 있지만, 패키지임포트가 나왔으니 짚고 넘어가자.

Go도 당연히 다른 언어처럼 작성한 코드의 재사용이 가능하다. 쉬이 예상 가능하듯, 패키지를 만들고 이를 임포트해 재사용할 수 있다.

그런데 Go는 C++이나 Java와는 달리 접근제한자(access modifier)가 없다. 접근제한자 대신 매우 단순한 한 가지 규칙에 따라 외부 접근 가능 여부를 판단한다.

외부 패키지에서 접근이 가능하게 하려면—export 하려면— 첫 번째 문자를 대문자로 하라.

이 규칙은 함수나 변수, 상수뿐만 아니라 구조체와 관련된 필드, 메서드 등에도 그대로 적용된다.

대문자에 대해서 좀 더 자세히 언급하면, Unicode “Lu” class에 포함되는 문자를 의미한다. 즉, 𝓐bc 이런 이름도 외부에서 접근할 수 있지만, 안타깝게도 한글은 포함되지 않는다.

Python도 접근제한자 없이 underscore(_)나 double underscore(__) 등을 사용해 접근 범위를 지정하지만 ‘우린 모두 19금을 볼 수 있지만 굳이 볼 필요가 없는 것은 안 볼 수도 있는 성숙한 어른이잖아요’ 라며 접근 시도를 자율에 맡기는 반면, Go는 아직 충분히 크지 않은 경우—대문자가 아닌 경우— 에는 접근을 강제로 불허한다.

패키지 만들기

서두에 언급한 것처럼 패키지를 만든다는 것이 아직 많이 이른 감은 있지만 패키지, 임포트에서 다룬 내용을 확실히 한다는 차원에서 간단히 알아보자.

디렉토리 구조는 다음과 같다.

$GOPATH -+- myapp  --- main.go
         |
         +- mypack -+- packa1 -+- packa1.go
                    |          |
                    |          +- packb.go
                    |
                    +- packa2 --- packa2.go

패키지 경로와 이름

임포트 에서 강조한 것처럼 관례상(by convention) 패키지의 경로와 이름을 같게 할 뿐, 둘은 엄연히 다르다.

위 디렉토리 구조에서 packa2, packa2.go, packb.go는 없고, packa1.go를 다음과 같이 작성한 상황을 가정해 보자.

// packa1.go
package packa

import "fmt"

func PackA1() {
	fmt.Println("This is PackA1()")
}

이 패키지를 사용하기 위한 main.go는 다음과 같다.

// main.go
package main

import "mypack/packa1"	// PATH

func main() {
	packa.PackA1()		// NAME
}

한 경로에 두 가지 이상의 패키지를 두면…

그럼 한 디렉토리에 있는 소스 파일들이 서로 다른 패키지 이름을 갖는다면 어떻게 될까?

위 예에서 packb.go 파일을 만들고 다음과 같이 작성한 것을 가정한 것이다.

// packb.go
package packb

import "fmt"

func PackB() {
	fmt.Println("This is PackB()")
}

이 경우 빌드 시 다음과 같은 오류가 발생한다.

found packages packa (packa1.go) and packb (packb.go) in ~~~/mypack/packa1

한 디렉토리에 다른 이름을 갖는 패키지들이 있다고 Go가 싫어한다.

한 패키지를 둘 이상의 경로에 두면…

반대로 같은 패키지 이름을 갖는 소스 파일들을 여러 경로에 분산해 두면 어떻게 될까?

packb.go는 삭제하고, packa2 디렉토리를 만들어 그 아래에 packa2.go를 다음과 같이 작성한 것을 가정한 것이다.

// packa2.go
package packa

import "fmt"

func PackA2() {
	fmt.Println("This is PackA2()")
}

PackA2() 를 호출하기 위해 main.go도 다음과 같이 수정한다.

// main.go
package main

import "mypack/packa1"
import "mypack/packa2"

func main() {
	packa.PackA1()
	packa.PackA2()
}

이 경우에는 빌드 시 다음과 같은 오류가 발생한다.

./main.go:5:8: packa redeclared as imported package name
	previous declaration at ./main.go:4:8
./main.go:8:2: undefined: "mypack/packa2".PackA1

mypack/packa2 경로에 있는 패키지를 임포트하면서 mypack/packa1에서 이미 임포트한 packa가 재정의 되었다는 오류와 이로 인해—mypack/packa2에서 임포트한 패키지가 mypack/packa1에서 임포트한 패키지를 덮어 씀으로 인해— 패키지 packa에서는 PackA1 함수를 찾을 수 없다는 오류이다.

이 문제는 임포트하는 패키지에 별칭(alias)을 달아 해결 가능하다.

// main.go
package main

import a1 "mypack/packa1"
import a2 "mypack/packa2"

func main() {
	a1.PackA1()
	a2.PackA2()
}

결론

관례라는 용어가 특수활동비 문제로 인해 답습과 비슷한 뜻으로 이해되기는 하지만 최소환 Go의 패키지 경로와 이름에 관련해서는 다 이유가 있어 생긴 것이다. 어떻게 되는지 봤으니 괜히 이상한 시도 하지 말자. 복잡해진다.