Loading
2022. 3. 21. 23:46 - lazykuna

[Golang] struct, tag, 그리고 reflect

Golang에서는 struct의 기능이 차지하는 비중이 큰데, 그 중 기존 프로그래밍에서 사용하는 interface, 캡슐화 같은 기능 뿐만 아니라, tag와 같은 특수한 기능이 눈에 띄는 것들인 것 같다.

사용법도 재미있는데, 기존 프로그래밍 언어들과 같이 명시/선언을 통해 그러한 기능들을 사용하는 것이 아니고 암묵적인 rule을 따라 사용하게 된다. Convention을 지킴으로서 자동으로 코드의 readability를 지키면서 동시에 원하는 기능을 구현한다는 점에서는 python의 indent convention과 유사한 점이 있는 듯 하기도 하다.

Interface

먼저 Interface 이야기. 상속 패턴은 어지간한 언어라면 다 지원하는 것들인데, golang도 예외는 아니다. 그리고, 상속 패턴은 보통 모체가 되는 원형 클래스가 존재하게 되고 이를 interface라고 부르곤 한다.

그런데, golang에서는 명시적으로 상속을 하지 않는다. 단지 interface에서 요구하는 메서드가 모두 구현되어 있기만 하면, interface의 상속 관계로서 인정해주는 형태다.

type TestImpl interface {
    Hello() string
}

type TestA struct{}

type TestStruct struct {
    test string `yaml:"test"`
}

func (t TestA) Hello() string {
    return "Hi!"
}

위 코드에서, TestA는 TestStruct가 요구하는 메서드를 모두 구현했으니까 상속 관계라고 암묵적으로 볼 수 있다.

Type casting

패턴에 맞는 interface가 구현되었다면, assign이 가능해진다. 이 모든 과정이 암묵적이라는 게 처음 봤을 때 참 신기했다...

이렇게 각 값이나 인스턴스의 실제(명시적) 타입과는 상관없이, 구현된 메서드로만 타입을 판단하는 방식을 덕 타이핑(Duck typing)이라고 한다. 덕 테스트에서 비롯된 말이라고 하는데, 이는 “어떤 새가 오리처럼 걷고 헤엄치고, 꽥꽥거리는 소리를 낸다면 이를 오리라고 정의하겠다"라는 이야기이다. 재미있는 철학이다.

func main() {
    var t TestImpl
    t = TestA{}
    fmt.Println("TestImpl.Hello says: ", t.Hello())
}

assign 하고 나서는 뭐... 그냥 다른 언어 쓰듯이 쓰면 된다.

Empty interface?

interface의 대표적인 기능의 prototyping에 대해 알아보았는데, 이 외에도 특이한 점이 또 있다. 비어있는 interface{} 를 사용할 수 있다는 건데, 이건 어디 쓰는 것인고 하니... Any type과 같은 기능을 한다.

var x interface{}
x = "abcd"
x = 1
fmt.Println("X is ", x)

타입이 뭐가 오든 신경쓰지 않고 뭐든지 다 잘 받아먹는 모습을 보여준다.

그럼 이걸 대체 어디 쓰느냐? 아무거나 Marshal 하고 싶을 때 쓴 적이 있다. 굳이 Marshal이 아니더라도 universal type을 받을 수 있는 경우 사용 가능할 듯. reflect 같은 거라던가...

var x interface{}
x = t  // some struct here...
a, _ := json.Marshal(x)
fmt.Println("marshaled like ", a)

그리고 empty interface에서 받은 타입을 원하는 타입으로 변경할 수도 있다. 실패할 경우 assert가 발생한다.

var x interface{}
x = 1
i := x.(int)

상속? 컴포지트?

아까 위에서는 인터페이스에 대해서 명시적인 상속을 사용하지 않는다고 했다. 하지만 명시적인 상속이 필요할 수도 있을 테다. 이를테면 특정 인터페이스 A의 기능을 구현한 AStruct가 있는데, 이를 가지고 AAStruct를 만들고 싶다던가...

struct는 보통 컴포지트 형태로 멤버변수를 만들어서 쓰는 게 보통이다. 이 경우에도 그렇게 할 수는 있겠지만, 아무래도 멤버 변수를 매번 호출해서 들어가는 것도 불편한 일이다. 이 경우를 위해, golang에서는 상위 struct의 멤버를 포함해주는 기능이 있다.

type AAStruct struct {
    AStruct
    ...
}

말은 거창한데 그냥 이렇게 쓰는게 끝이다... 형(type)만 쓰고 이름은 없다.

이렇게 상속(?)받은 struct의 멤버(또는 메서드)에 직접 접근할 수도 있고, 혹은 컴포지트처럼 접근할 수도 있다.

// AStruct has method run()
AAStruct a
a.run()
a.AStruct.run()

Encapsulation

Golang struct의 멤버의 encapsulation은 멤버 변수명으로 결정된다.

일단 Golang의 네이밍 컨벤션은 기본적으로 camel case를 따르고 있는데, 여기에서 앞 글자가 대문자면(=Pascal case) public method, 소문자면 private가 된다.

그리고 이러한 Encapsulation의 기준은 package가 된다. 즉 내부 패키지면 private라도 접근이 가능하고, 외부 패키지면 public만 접근이 가능하다.

package A

type Test struct {
    variableA int
    VariableA int
}

...

package B

t := Test{1, 2}
fmt.Println(t.variableA) // error
fmt.Println(t.VariableA)

그리고 위에서 struct만 encapsulation이 된다고 적어놨는데... go파일 내 전역 변수/구조체 또한 마찬가지 규칙을 적용받는다.

Tag와 Reflect 패키지

이건 기존의 프로그래밍 언어에서 보기 어려웠던 점이라고 생각되는데, 기존 프로그래밍 언어에서는 type의 메타정보를 추가하기 위해서는 상속이니, 템플릿이니 보통 난리를 쳐야 했고, 사용하는 것도 그에 비견할 정도로 까다로웠기 때문이다.

하지만, Golang은 다르다. Tag라는 Extra meta information을 달 수 있는 영역이 존재한다. 사용법도 간단하다. 이를테면, 어떤 변수의 “yaml” attribute를 “test”로 지정하고 싶다면,

type TestStruct struct {
    test string `yaml:"test"`
}

이걸로 충분하다.


헉쓰~ 간단하네

비슷한 기능을 제공하는 다른 언어가 있는지는 모르겠는데... 내가 아는 바로는 저렇게 type에 정보를 새로 담으려면 새로운 type을 만들고, 이를 사용하기 위해 추가로 코드를 또 넣어야 해서 꽤나 귀찮다. 아니면, std::pair 같이 추가 정보를 담을 컨테이너를 만들어 주어야 할 듯?

Reflect으로 Tag 정보 가져오기

그러면 여기에 담은 정보는 어떻게 가져올 수 있는가? reflect 패키지가 있다. 아래처럼 field들을 돌면서 tag를 가져올 수 있다.

func extract(obj interface{}) {
    elems := reflect.ValueOf(obj).Elem()
    for i := 0; i < elems.NumField(); i++ {
        field := elems.Field(i)
        typefield := elems.Type().Field(i)
        tag := typefield.tag
        fmt.Println("%v Field -- value(%v), tag(%v)",
            typefield.Name, field.Interface(), typefield.Type)
    }
}

여기서는 간단히 struct의 정보만 뽑아 봤는데, 메서드 정보 등 더 많은 정보들을 뽑아올 수 있다. 자세한 내용은 인터넷에 많이 있으니 링크로 대체...

두 개의 tag를 동시에 사용하는 것도 가능하다. 또한 Reflect에서 태그별로 쉽게 값을 가져올 수 있다.

type TestStruct struct {
    test string `yaml:"test" json:"testjson"``
}

...

typefield := elems.Type().Field(i)
tag := typefield.tag
fmt.Println("json Tag: %v", tag.Get("json"))

보다 실용적인 용도

물론, 보다 실용적인 용도는 tag를 이용하여 json이나 yaml 파일을 읽어들이는 용도일 것이다. 이건 정말 좋은 것 같다. 굳이 애써 variable name이랑 stringify된 json attribute name을 일일이 정해 놓을 필요가 없고, nested struct에서도 똑같이 적용이 가능하니 ...

jsonMarshalled = `{
    "test": "Hello!"
}`

json.Unmarshal([]byte(jsonMarshalled), &testStruct)

그런데 이걸 meta라고 불러야 하나?

그런데 여기까지 오니까 궁금증이 생겼다. 보통 meta 정보는 Compile-time에만 존재하고 runtime에는 존재할 수 없는 정보여야 한다. 하지만 for loop 등을 타서 tag iterate를 돌 수 있는 구조라면 runtime에도 정보가 존재하는 것 같은데...

그런데 그렇다면 tag는 type의 meta 정보라고 볼 수 있을까? 사실상 2-pair tuple이랑 같은 것 아닌가 싶다. 실제로 struct의 tag가 달라도 실제 type이 일치한다면 compatible type으로 취급하여 assign도 된다.

그래도 특이한 점은 tag 정보는 immutable 하다는 점이다. Stackoverflow들에 관련 질문들이 많이 올라와 있는 것을 확인 할 수 있었다. 나름 무언가 특수 처리가 되어 있는 듯.

당장 편의성을 챙길 수 있다는 점은 확실히 좋지만, 굳이 immutable 한걸 보아서 뭔가 더 있지 않을까 싶다. 그게 구체적으로 무엇인지는 나중에 실험으로 알 수 있으면 좋겠다. (pre-evaluation이 되는지 등...)

실험: TODO

출처

'개발 > Developing' 카테고리의 다른 글

[Golang] Context 패키지  (0) 2022.03.27
[Golang] defer과 value capture에 대해서  (0) 2022.03.26
[Linux] rsync  (0) 2022.03.17
함수형 프로그래밍과 멀티 코어  (0) 2022.03.17
Compiler Explorer  (0) 2022.03.05