티스토리 뷰

프로그래밍 언어/go

리플렉션

4whomtbts 2018. 12. 26. 20:29

컴퓨터사이언스에서 리플렉션이란, 프로그램이 자체의 구조를 타입에 대해서 시험해볼 수 있는 능력을 의미합니다.

Go같은 경우는, 정적타입 언어이지만, 인터페이스라는 도구가 있어서 정적타입임에도 불구하고 상당히 유용하게 사용할 수 있는

언어인데요, 이런 인터페이스 자료형을 함수의 매개변수로 넘기고 사용하는 과정에서 자료형을 명시해야하는 경우가 왕왕있습니다. 

그런 경우에 리플렉션을 사용할 줄 모른다면, 프로그램 구조가 경직되거나 기껏 짜놓은 코드가 자료형을 핸들링하지 못 해서 

갈아엎어야 하는 경우가 있습니다. 물론 리플렉션을 사용하는 패턴은 가능하면 피해야 하는 것이 맞습니다. Rob pike 는 

https://www.youtube.com/watch?v=PAAkCSZUG1c&t=15m22s 에서,  "리플렉션은 절대 clear(명료하거나, 간결한)지 않다" 라고 말했습니다.

그렇지만, 한 편으로는 리플렉션은 Go 언어에서 많은 경우에 불가능한 것을 가능하게 만들어주기도 합니다 . 또한 앞서 말씀드렸듯이

interface{} 라는 자료형이 있기 때문에 사용이 불가피한 경우가 많지만. 가능하면 사용을 지양해야 합니다. 리플렉션 패키지의 모든 메소드는 

에러에 상당히 엄격하기 때문에 상당히 많은 에러 체크를 해야하기 때문에  코드의 가독성이 떨어집니다.

대부분의 에러가 panic 을 발생시키기 때문에 에러에 대한 위험부담이 큽니다.

우선, 리플렉션의 가장 기본적인 feature 부터 보겠습니다.

ㅁrefelction.TypeOf & reflection.Type.Kind()

type MyInt int
var a int
var b MyInt
fmt.Println("a 의 타입 ", reflect.TypeOf(a))
fmt.Println("b 의 타입 ", reflect.TypeOf(b))
fmt.Println("a Kind ", reflect.TypeOf(a).Kind())
fmt.Println("b Kind ", reflect.TypeOf(b).Kind())

TypeOf 는 말 그대로 변수의 자료형을 말 해줍니다. 


a 의 타입 int
b 의 타입 main.MyInt

Kind 도 자료형을 말해주는 것은 비슷하지만, underlying 된 자료형을 알려줍니다.

MyInt 는 결국에는 Int 형이기 때문에 Kind 의 결과는 아래와 같습니다.

a  Kind  int
b Kind int

다른 자료형도 보겠습니다.

type MyInt int
var a int
var b MyInt
type newStruct struct{}
testStruct := newStruct{}
var newInf interface{}


fmt.Println("a 의 타입 ", reflect.TypeOf(a))
fmt.Println("b 의 타입 ", reflect.TypeOf(b))
fmt.Println("a Kind ", reflect.TypeOf(a).Kind())
fmt.Println("b Kind ", reflect.TypeOf(b).Kind())
fmt.Println("newStruct 의 타입 ",reflect.TypeOf(testStruct))
fmt.Println("newStruct Kind ",reflect.TypeOf(testStruct).Kind())
fmt.Println("newInf 의 타입 ",reflect.TypeOf(newInf))
결과
a 의 타입 int
b 의 타입 main.MyInt
a Kind int
b Kind int
newStruct 의 타입 main.newStruct
newStruct Kind struct
newInf 의 타입 <nil>
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x98 pc=0x48bb14]

goroutine 1 [running]:
main.main()
D:/workspace/goprac/main.go:97 +0x5c4

빈 인터페이스스에 대한 Kind 의 결과는 에러를 리턴합니다.  하지만 

newInf = "I'm interface"

를 추가하면 

newInf 의 타입  string
newInf Kind string

이러한 결과를 얻을 수 있습니다. reflect 패키지의 메소드들 대부분은 빈 인터페이스(interface{}) 에 대해서 invalid 한 결과를 리턴하거나

패닉을 발생시킵니다. 

ㅁ Settability 와 Addressability 

리플렉션에는  Settability 와 Addressability 란 것이 존재합니다. 우선 코드부터 보겠습니다.


var a float64
a = 7.7
fmt.Println("a 의 값 ",a)
ra := reflect.ValueOf(a)
ra.SetFloat(12.5)
fmt.Println("ra 의 값 ", ra)

이 코드는 직관적으로 보았을 때 당연히 동작할 것 같습니다. 그렇지만 이것은 패닉을 발생시킵니다, 오류 메세지를 보면 

panic: reflect: reflect.Value.SetFloat using unaddressable value

unaddressable 한 값에 reflect.Value.SetFloat 을 사용할 수 없다고 합니다.

ㅁ addressable 과 settable 이란 무엇인가?

 -> addressable 은 표현식으로 주소가 얻어질 수 있는 것.

 그렇다면, 왜 위의 코드가 유효하지 않은지 알아보겠습니다. 

 이는 stackoverflow 의 관련답변입니다

[출처 : https://stackoverflow.com/questions/48790663/why-value-stored-in-an-interface-is-not-addressable-in-golang ]

인터페이스 안의 값은 숨겨진 메모리 공간에 저장되기 때문에, 컴파일러는 자동적으로 그 메모리를 참조할 수 없습니다 

그래서, 인터페이스 안에 저장되어진 값은 addressable 하지 않지만, 그것은 정확히는 "숨겨진 메모리 영역" 에 

있는 것은 아닙니다. 이에 대한 일반적인 답변은 아래와 같습니다.

"인터페이스 값이 생성되어질 때, 인터페이스에 쌓여져있는 값이 복사됩니다. 따라서 그것의 주소를 얻을 수없고

행여 그럴 수 있다고 하더라도, 인터페이스에 대한 포인터 참조는 예상치 못한 영향을 야기합니다

(예를들어, 원래 복사된 값을 변경할 수 없습니다) "

이러한 것은, 이해하기 어렵습니다. 왜냐하면 인터페이스에 복사된 값에 대한 포인터는 정적 타입(concrete)에 대해 복사된

값의 포인터와 다를게 없기 때문입니다. 이러한 두 경우에, 복사된 값에 대한 포인터로 원래의 복사된 값을 바꿀 수는 없습니다.

그러면, 왜 인터페이스에 저장된 값은 addressable 하지 않을까요? 그 답은 아래의 예시에서 "만약 addressable 하다는" 가정하에 세워진

예시에서 보겠습니다. 



자, 우리가 I 라는 인터페이스를 가지고 있다고 칩시다. 그리고 또 두 타입 A,B가 있습니다. 

A를 생성하고, I 에다가 저장해보겠습니다.

이제 우리가 인터페이스에 담겨있는 주소를 참조할 수 있다고 가정해보죠

aPtr = &(i.(A))는 불가능하지만,  우리가 그럴 수 있다고 가정해보면

B를 생성하고 i에다가 저장해보겠습니다.


이것이 결과인데요,

B를 i에다가 대입하면, aPtr 은 A를 가르키도록 선언되었는데, 이제는 B를 포함하고 aPtr 은 더이상 A의 유효한 포인터가 아닙니다.

하지만 아래의 것은 허용되는데요 허용되는데요. 왜냐하면, 두번 째 라인은 i.(A)의 복사된 값을 저장하고 따라서

aPtr 이 i.(A) 를 가리키는 것이 아니기 때문입니다.

그렇다면 왜 왜 포인터 값을 포함하지 않는 인터페이스는 포인터 리시버를 갖는 메소드의 리시버가 될 수 없을까요? 

그것은 바로, 인터페이스에 포인터가 아닌 값이 저장됬고 이것은 addressable 하지 않기 때문에

컴파일러가 이것의 주소를 포인터 리시버에게 전달할 수 없는 것입니다. 


 위의 설명이 충분한가요 ? 저는 명확하게 이해가 되지 않아서, 타이핑을 해보았습니다.


type A int
type I interface{}
var a A = 5
var i I
fmt.Printf("i %T\n",i)
i = a

var aPtr *A
var a2 A = i.(A)
fmt.Printf("i %T\n",i)
fmt.Printf("atpr %T\n",aPtr)
fmt.Printf("a2 %T\n",a2)

이 코드의 결과는

결과
i <nil>
i main.A
aptr *main.A
a2 main.A

이렇습니다.  i 는 처음 만들어졌을 때 타입이 없는데요 , a 의 값을 대입하고 난 뒤 A의 타입을 갖습니다.

 그리고 A 의 포인터 타입변수 aPtr 을 선언하고, a2 에다가 i 를 A로 conversion 한 값을 넣어줍니다. 

그러면 a2 는 분명히 main.A 의 타입을 갖기 때문에 aPtr 도 유효한 것 맞죠?

그런데 위에서 주장하는 것은(주석을 잘 보세요)

type A int
type B int
type I interface{}
var a A = 5
var i I
i = a

var aPtr *A
aPtr = &(i.(A))
var b B = "Hello"
i = b // 이 시점에서 i 의 타입은 main.B 가 되겠죠?
/*
만약 이 코드가 컴파일 가능하다고 하면
i main.B 의 타입인데, aPtr *main.A 타입이 되버리므로
유효한 포인터가 아니게 되는겁니다
*/

이러한 일이 발생할 수 있기 때문에 인터페이스에 저장된 값은 addressable 하지 않습니다. 

여기까지가 addressable 이었습니다. 

그러면 

Settability 란 무엇인가?

이것은, addressability 랑 비슷하지만 조금 더 엄격합니다. 

Settability 란 리플렉션 객체가 실제(원래)의 아이템을 가지고 있는지에 의해 결정되며, CanSet 메소드를 통해 여부를 알 수 있습니다.

결론적으로는 , GO 에서는 이러한 행위(참조를 얻어서 간접연산으로 값을 바꾸더래도, 실제로 원시객체의 값은 변하지 않는 현상)

은 무의미하고, 혼란스럽기 때문에 이러한 문제를 피하기 위해서 아예 illegal 하게 설정해놓았습니다.


so it is illegal, and settability is the property used to avoid this issue



addressability 와 settability 에 대한 더 자세한 예를 보고싶으시다면, https://play.golang.org/p/FrMtB0szvoD




앞의 예제로 돌아가


var a float64
a = 7.7
fmt.Println("a 의 값 ",a)
ra := reflect.ValueOf(a)
ra.SetFloat(12.5)
fmt.Println("ra 의 값 ", ra)

에서 ValueOf 의 정의는

func ValueOf(i interface{}) Value
와 같습니다, 그러니깐 우리가 
ra := reflect.ValueOf(a)

했을 때는, a 는 interface 타입의 값이 되기 때문에 addressable 하지 않고, 당연히 원시변수(a) 를 가지고 있지 않으므로

Settability 를 충족하지 않기 때문에 에러를 얻는것이었습니다.

그래서 우리는 a의 참조를 넘기겠습니다

var a float64
a = 7.7
fmt.Println("a 의 값 ",a)
ra := reflect.ValueOf(&a)
fmt.Println("type of ra:", ra.Type())
fmt.Println("settability of ra:", ra.CanSet())

그런데, 이것도 우리가 원하는 결과를 얻지는 못 합니다.

a 의 값  7.7
type of p: *float64
settability of p: false

왜냐하면, ra 에는 a의 주소값이 저장되어 있을 뿐이고, 우리가 건드리고 싶은것은 *ra 이기 때문입니다. 

그런데, *ra 는 유효하지 않습니다. 왜냐하면  ValueOf 의 리턴타입은 Value 이기 때문입니다. 따라서 우리는 *ra 를 얻기위해서

Elem() 이라는 메소드를 사용해서 얻을 수 있습니다

v := ra.Elem()
fmt.Println("v:", v)
fmt.Println("type of v:", v.Type())
fmt.Println("settability of v:", v.CanSet())

이렇게 하면 원하는 결과를 얻습니다

결과
v: 7.7
type of v: float64
settability of v: true


이제 드디어 setFloat 을 쓰면

v.SetFloat(33.33)
fmt.Println(v.Interface())
fmt.Println(a)

결과
33.33
33.33

을 얻으므로 성공했습니다.

여기까지 아주 기본적인 reflection 에 대해 알아보았고, 이제 reflection 을 어떻게 활용할 수 있는지 보겠습니다.


ㅁ reflection 사용례


func (docker *Docker) getMethod(name string) Cmd {
methodName := "Cmd"+strings.ToUpper(name[:1])+strings.ToLower(name[1:])
method, exists := reflect.TypeOf(docker).MethodByName(methodName)
if !exists {
return nil
}
return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
ret := method.Func.CallSlice([]reflect.Value{
reflect.ValueOf(docker),
reflect.ValueOf(stdin),
reflect.ValueOf(stdout),
reflect.ValueOf(args),
})[0].Interface()
if ret == nil {
return nil
}
return ret.(error)
}
}
type Cmd func(io.ReadCloser, io.Writer, ...string) error

위 코드는 Docker 의 initial Commit 에서 발쵀해온 코드의 일부입니다

이 코드를 간단하게 설명하자면, 

1. 터미널에서 명령을 입력받는다 

-> 

method 이름을 사용자가 입력한 argument 를 이용해서 설정한다  

( 제가 showConfig 를 입력했다고 가정해보면,  methodName 에는 CmdShowconfig 가 저장되는 식입니다. )

그래서 이런식으로 네이밍된 메소드들이 존재합니다.

func (docker *Docker) CmdMirror(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
_, err := io.Copy(stdout, stdin)
return err
}

->

2. 변수 method 에는 

   method, exists := reflect.TypeOf(docker).MethodByName(methodName)

docker 는 *Docker 타입이기 때문에, 참조를 전달해주었고, MethodByName 은 해당 구조체의 메소드가 있으면 그 메소드를 반환해주고

exists 를 true 로 설정하고 그렇지 않다면 exists 를 false 로 설정합니다.


return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
ret := method.Func.CallSlice([]reflect.Value{
reflect.ValueOf(docker),
reflect.ValueOf(stdin),
reflect.ValueOf(stdout),
reflect.ValueOf(args),
})[0].Interface(

그리고, Cmd 타입의 함수를 반환해주고, CallSlice 함수를 호출하는데요, 이것은 variadic(인수의 개수가 런타임에 결정되는) 함수에만 사용할 수

있습니다. 


이런식으로 reflection 을 사용해서 런타임에 어떤 메소드를 호출할 것인지 결정하고, 인자도 가변적으로 보낼 수 있습니다.

 

아래는 제가 만들어본 예제입니다. 이 프로그램은 효율적이거나 합리적이지는 않지만 억지로 리플렉션을 쓰기 위해 만들어봤습니다.

우리는 두 개의 데이터베이스가 있습니다. 하나는 상품에 관한 정보를 담은 데이터베이스이고, 하나는 인사관리를 위한 데이터베이스입니다.

그런데, 이러한 데이터베이스가 너무 많아져서 메소드가 지나치게 많아지고 점점 헷갈리기 시작합니다. 그래서 우리는 각자 데이터베이스에

맞는 메소드를 호출하기 보다는, 데이터 정보를 보내면 알아서 쿼리를 생성해주는 인터페이스를 만들 것 입니다.


type UniversalQueryStatement interface {
GetQuery(args ...interface{})
}
type Employ struct {
Name string
Authority string
}
type Product struct {
Name string
Price int
}
// 아래 각각의 구조체의 메소드들이 GetQuery 를 구현했기에 UniversalQueryStatement implement.
func (E *Employ) GetQuery(args ...interface{}) {
fmt.Printf("fake Query SELECT %s %s",args[0],args[1])
}
func (P *Product) GetQuery(args ...interface{}) {
fmt.Printf("fake Query : INSERT FROM %s TO %d",args[0],args[1])
}

Employ 와 Product 구조체는 UniversalQueryStatement 를 implement 합니다. 

GenerateQuery 함수를 작성해서 아래와 같이 main 함수를 만들고 싶습니다


func main() {
p := Product{
Name : "salt",
Price : 300,
}


GenerateQuery(&p)

}


func GenerateQuery( q interface{}) {

TypeOfQuery := reflect.TypeOf(q) // q의 타입을 받습니다.
ValueOfQuery := reflect.ValueOf(q)
fmt.Println("쿼리타입 : ",TypeOfQuery)
fmt.Println("쿼리값 : ",ValueOfQuery)
mock := []interface{}{} // 메소드에 넘겨줄 interface{} 타입의 슬라이스입니다


for i := 0; i < TypeOfQuery.Elem().NumField(); i++{ // NumField 는 필드의 갯수를 리턴해줍니다.

element := ValueOfQuery.Elem().Field(i).Interface()
fmt.Println(element)
mock = append(mock,element) // mock 에 우리가 받은 구조체의 필드값을 append 해줍니다
}

if reflect.TypeOf(q).Kind() != reflect.Ptr{
fmt.Println("구조체 포인터만 받을 수 있습니다")
}



method , exists := reflect.TypeOf(q).MethodByName("GetQuery") // q가 뭔지는 모르지만(우리가 프로그램을 작성하는 시점에)

// GetQuery 가 존재한다는 것은 미리 알기에, q 의 GetQuery 를 불러주도록 합니다.


if !exists {
fmt.Println("메소드가 존재하지 않습니다")
return;
}

// mock 슬라이스를 인자로 넘겨서 함수를 실행해줍니다.
method.Func.CallSlice([]reflect.Value{
reflect.New(TypeOfQuery).Elem(),
reflect.ValueOf(mock),
})

}

결과
쿼리타입 : *main.Product
쿼리값 : &{salt 300}
salt
300
fake Query : INSERT FROM salt TO 300


참고 

https://blog.golang.org/laws-of-reflection

https://stackoverflow.com/questions/48790663/why-value-stored-in-an-interface-is-not-addressable-in-golang

'프로그래밍 언어 > go' 카테고리의 다른 글

go get 오류 exit 128  (0) 2019.02.06
댓글