reflect를 사용하는 방법
Go에서 reflect
패키지를 이용하면 조금 더 많은 일을 할 수 있다. 이름에서 느껴지듯 reflect
는 런타임에서 뭔가를 처리할 수 있게 해 주므로, 준비만 철저하다면 굉장히 효율적으로 동작하도록 코드를 작성할 수 있는 것이다. 여기서는 간단히 reflect를 어떻게 사용할 수 있는지, 그리고 어떻게 응용할 수 있는지를 간단히 정리해 보자.
reflect의 Type과 Value
어떤 변수의 값을 reflect
로 분석하는 경우, reflect에서는 그 어떤 값을 크게 두 가지의 분류로 나누어 확인할 수 있다. 각각 Type
과 Value
가 되겠다.
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.Kind
는 reflect.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
로 변경된다.