Go에서 reflect패키지를 이용하면 조금 더 많은 일을 할 수 있다. 이름에서 느껴지듯 reflect는 런타임에서 뭔가를 처리할 수 있게 해 주므로, 준비만 철저하다면 굉장히 효율적으로 동작하도록 코드를 작성할 수 있는 것이다. 여기서는 간단히 reflect를 어떻게 사용할 수 있는지, 그리고 어떻게 응용할 수 있는지를 간단히 정리해 보자.

reflect의 Type과 Value

어떤 변수의 값을 reflect로 분석하는 경우, reflect에서는 그 어떤 값을 크게 두 가지의 분류로 나누어 확인할 수 있다. 각각 TypeValue가 되겠다.

reflect의 Type

Type은 실제로 우리가 알고 있는 Go에서의 그 타입을 말한다. 이게 왜 필요하냐 하면 Go의 interface에는 어떠한 타입이든 담을 수 있으므로, 실제로 인터페이스에 어떤 타입이 들어갔는지를 확인하기 위해서이다. 그래서 그런 경우에는 보통 reflect.TypeOf를 사용한다.

func TypeOf(i interface{}) Type

interface i의 타입을 reflect.Type형태로 리턴한다. 그럼 Type은 무엇인가? reflect에서 더욱 더 효율적으로 어떤 것을 처리하기 위해 만든 어떤 wrapper 정도로 생각해 두자.

package main

import (
	"fmt"
	"reflect"
)

type TestInt int

func main() {
	var i TestInt
	var j int
	i = TestInt(4)
	j = 4
	
	fmt.Println(reflect.TypeOf(i))
	fmt.Println(reflect.TypeOf(j))
}

이 코드의 출력은 다음과 같다.

$ go run main.go
main.TestInt
int

커스텀 타입 TestInt는 go의 primitive가 아니므로, 어디에서 정의된 것인지를 보여주기 위해 패키지 main이 포함되었다.

reflect의 Value

이번엔 reflect.Value를 살펴보자. Value도 Type과 마찬가지로 어떤 값을 wrapping해둔 것이라고 생각하면 편하다. 어쨌든 Value는 실제로 여러가지 값을 처리할 수 있는 함수를 제공하는데, 예를 들면 대략 이런 식이다.

  • Value.Int(): Value가 int타입이었다면 int 값 리턴
  • Value.String(): Value가 String타입이었다면 string 값 리턴(다른 것보다 String은 내부적으로 조금 더 복잡한 것 같다…)

ValueOf를 사용하면 어떤 인터페이스로부터, 가지고 있는 값 Value를 꺼낼 수 있다.

func ValueOf(i interface{}) Value

interface i를 받아 reflect.Value를 리턴한다. 아래와 같이 사용한다.

package main

import (
	"fmt"
	"reflect"
)

type TestInt int

func main() {
	var i TestInt
	var j int
	i = TestInt(4)
	j = 4
	
	fmt.Println(reflect.ValueOf(i))
	fmt.Println(reflect.ValueOf(j))
}

이 코드의 출력은 다음과 같다.

$ go run main.go
4
4

reflect.Kind

reflect.Kindreflect.Value, reflect.Type에 대해서, 실제로 어떤 자료형으로 이루어져있는지를 확인할 수 있는 함수다. reflect.TypeOf와는 다른데, reflect.TypeOf는 그냥 그 형태로 타입이 어떤 것인지를 확인하는 것이라면 reflect.Kind는 미리 정의된 자료형 중 어떤 것인지를 확인하는 의미가 큰 것 같다. 따라서 reflect.Kind로는 go에서 사용하는 underlying 자료형만 출력할 수 있고, 이 리스트는 여기서 확인할 수 있다.

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

따라서 아래 코드를 실행하면 TestInt도 그 underlying인 int로 결과가 나올 것이다.

package main

import (
	"fmt"
	"reflect"
)

type TestInt int

func main() {
	var i TestInt
	var j int
	i = TestInt(4)
	j = 4
	
	fmt.Println(reflect.ValueOf(i).Kind())
	fmt.Println(reflect.ValueOf(j).Kind())
}
$ go run main.go
int
int

reflect.Value의 다양한 기능들

reflect.Value에는 다양한 함수들이 존재한다. 이를 적절히 사용할 수 있다면 여러 가지 일을 더 효율적으로 처리할 수 있다. 실제로 이런 식으로 코드를 작성하지는 않겠지만 간단히 예를 들어 보면 아래와 같은 코드를 써볼 수 있다.

package main

import (
	"fmt"
	"reflect"
)

type TestInt int

func main() {
	var i int
	i = 4
	
	var testInt TestInt
	testInt = reflect.ValueOf(i).Convert(reflect.TypeOf(testInt)).Interface().(TestInt)
	// testInt = i
	
        fmt.Println(reflect.TypeOf(i))
	fmt.Println(i)
	
	fmt.Println(reflect.TypeOf(testInt))
	fmt.Println(testInt)
}

testInt에 i를 직접 할당할 경우 cannot use i (type int) as type TestInt in assignment 와 같은 에러 메시지가 출력될 것이다. 그래서 reflect.Convert는, 어떤 Value를 Type으로 Convert한 Value를 결과로 리턴해 준다.

func (v Value) Convert(t Type) Value

reflect.Interface는 어떤 Value를 우리와 친숙한 interface타입으로 리턴해 준다. CanInterface와 함꼐 쓰면 더욱 좋을지도?

func (v Value) Interface() (i interface{})
func (v Value) CanInterface() bool

따라서 위 코드는 변수 i의 Value를 testInt 타입으로 변환한 후 이의 interface를 취해서 type assertion을 진행한 것.

아무튼 reflect.Value에 있는 다양한 함수들은 한번씩 봐둘 필요는 있다.

reflect.Value로부터 함수 호출

reflect로부터 이런저런 정보를 얻은 후 그에 맞추어 함수(이하 메소드)를 직접 호출할 수도 있다. Interface() 후 type assertion을 통해 진행해도 되지만 우선 호출할 수 있는 방법을 간단히 살펴보자.

reflect.Type에는 Method를 찾을 수 있는 여러가지 방법이 있다.

  • reflect.Type
    • Method(int) Method
    • MethodByName(string) (Method, bool)
    • NumMethod() int

이런 식으로 써볼 수 있다. Method(int)는 reflect.Method 타입을 리턴한다.

package main

import (
	"fmt"
	"reflect"
)

type TestStruct struct {
    TestStructValue int
}

func (t TestStruct) String() string {
    return fmt.Sprintf("TestStructValue is %d", t.TestStructValue)
}

func (t *TestStruct) Increment() {
    t.TestStructValue = t.TestStructValue + t.one()
}

func (t *TestStruct) one() int {
    return 1
}

func main() {
	t := &TestStruct{TestStructValue: 1}
	
	rt := reflect.TypeOf(t)
	
	numMethod := rt.NumMethod()
	
	fmt.Println("NumMethod: ", numMethod)
	
	l := 0
	for l < numMethod {
		m := rt.Method(l)
		fmt.Println("Method Name?: ", m.Name)
		fmt.Println("Method Type?: ", m.Type)
		fmt.Println("Method Func?: ", m.Func)
		fmt.Println("Method Index?: ", m.Index)
		fmt.Println("Method Pkg?: ", m.PkgPath)
		
		l = l + 1
	}
}

그러면 결과는 다음과 같다.

$ go run main.go
NumMethod:  2
Method Name?:  Increment
Method Type?:  func(*main.TestStruct)
Method Func?:  0x4bdfe0
Method Index?:  0
Method Pkg?:  
Method Name?:  String
Method Type?:  func(*main.TestStruct) string
Method Func?:  0x4be580
Method Index?:  1
Method Pkg?:

만약 t가 포인터가 아닌 형태의 TestStruct를 저장했다면 찾을 수 있는 함수는 String()만 있을 것이다. 그리고 export되지 않은 메소드 one은 표현되지 않았는데, 따라서 찾을 수 있는 함수는 반드시 export되어야 한다는 것이다.

어쨌든 이를 통해 함수의 이름을 알았으므로 우리는 간단히 이름을 통해 함수를 호출할 수 있다.

package main

import (
	"fmt"
	"reflect"
)

type TestStruct struct {
    TestStructValue int
}

func (t TestStruct) String() string {
    return fmt.Sprintf("TestStructValue is %d", t.TestStructValue)
}

func (t *TestStruct) Increment() {
    t.TestStructValue = t.TestStructValue + t.one()
}

func (t *TestStruct) one() int {
    return 1
}

func main() {
	t := &TestStruct{TestStructValue: 1}
	
	rt := reflect.ValueOf(t)
	
	m := rt.MethodByName("Increment")
	m2 := rt.MethodByName("String")
	
	m.Call([]reflect.Value{})
	m.Call([]reflect.Value{})
	fmt.Println("call from reflect.TypeOf: ", m2.Call([]reflect.Value{}))
}

Call()로 호출할 메소드에 전달할 파라미터는 반드시 []reflect.Value로 전달하여야 한다. 정의가 그렇다.

func (v Value) Call(in []Value) []Value

파라미터가 없는 메소드라 하여도 빈 값을 넣어야 한다.

Example: 구조체의 대표 이름 출력하기

예를 들어 어떤 구조체가 있는데 이 이름을 통해 어떤 스트링을 출력해야 한다고 가정하자. 어떤 값이 있다면 그 값을 리턴하고 값이 없다면 구조체의 이름을 그대로 리턴하면 된다. 그러나 문제는 이 구조체가 단순히 구조체로만 오는 것이 아니라, 구조체의 배열로 오는 경우도 있다는 것이다. 만약 구조체만 오는 경우 대충 변환해서 이름을 찍으면 되지만, 배열이라면? 거기다 배열이 빈 채로 온다면 이게 참 답답하게 될 것이다.

	Username string
}

type Email struct {
	Emailname string
}

func (e *Email) ModelName() string {
	return "myemail"
}

func main() {
	var (
		result string
	)

	u := &User{Username: "chad"}
	result, _ = NameExtractor(u)
	fmt.Println("struct User: ", result)

	e := &Email{}
	result, _ = NameExtractor(e)
	fmt.Println("struct Email: ", result)

	userarr := &[]User{}
	result, _ = NameExtractor(userarr)
	fmt.Println("[]struct User: ", result)

	emailarr := []Email{}
	result, _ = NameExtractor(emailarr)
	fmt.Println("[]struct Email: ", result)

}

func NameExtractor(v interface{}) (string, error) {
	valueType := reflect.ValueOf(v).Type()

	if IsPtr(valueType) || IsSlice(valueType) {
		valueType = valueType.Elem()
	}
	if IsPtr(valueType) || IsSlice(valueType) {
		valueType = valueType.Elem()
	}

	m := reflect.New(valueType)
	if n, ok := m.Interface().(Modeler); ok {
		return n.ModelName(), nil
	}

	fmt.Println("valueType?: ", valueType)
	return valueType.Name(), nil
}

func IsPtr(t interface{}) bool {
	return compareKind(t, reflect.Ptr)
}

func IsSlice(t interface{}) bool {
	return compareKind(t, reflect.Slice)
}

func compareKind(t interface{}, kind reflect.Kind) bool {
	switch v := t.(type) {
	case reflect.Type:
		return v.Kind() == kind
	case reflect.Value:
		return v.Kind() == kind
	}

	return false
}

그 실행 결과는 아래와 같다.

$ go run main.go
valueType?:  main.User
user:  User
emailarr:  myemail
valueType?:  main.User
userrr:  User
email:  myemail

사실 이런 식의 동작은 이미 Gorm에서 사용하고 있다. 객체에 쿼리 결과를 바인딩할 때, 테이블 이름을 따로 입력하지 않아도 처리할 수 있게 하기 위해서이다. 그래서 넘겨받은 구조체의 이름을 파악하고 그것을 테이블 이름으로 사용하는 것이다. 거기에 추가로 TableName() 함수를 구현할 경우, 이 결과를 구조체 이름 대신 테이블 이름으로 사용하는 것이 바로 gorm의 동작 방식이다.

func New(typ Type) Value

이 함수는 어떤 reflect.Type을 받아 그 typ에 맞는 새로운 포인터 타입의 reflect.Value를 리턴해 준다. 즉, 뭔지는 모르는 x를 생성한다면 New()는 결국 new(x)와 같은 동작을 한다는 것.

func (v Value) Elem() Value

이 함수는 v가 가리키는 뭔가를 리턴한다. 만약 v가 포인터라면, 포인터가 가리키는 어떤 값을 리턴한다. 특이하게 v가 인터페이스라면 그 인터페이스가 폼하하고 있는 뭔가를 리턴해준다. 따라서 포인터가 온다면 포인터가 가리키는 것, 배열이라면 배열의 한 개의 요소를 리턴한다.

그래서 만약 NameExtractor&[]User{}가 들어올 경우, 첫 번째 IsPtr 에서 []main.User로 변경되며 이후의 IsPtr에서 main.User로 변경된다.