- 출처: https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/reflection
- 개요
reflect package에 대해 알아본다. struct x을 받고 fn을 호출해서 재귀적으로 내부의 string field들을 모두 순회하는 함수 walk(x interface{}, fn func(string))를 작성해보자. 이번 챕터에서는 이 요구사항을 구현하기위해 reflection을 사용해볼것이다.
- interface
여태까지는 string, int 형 처럼 Go에서 제공하거나, BankAccount와 같이 형을 정의한 type-safety만 다루었다. 그래서 참조할 문서를 쉽게 찾을 수 있었고, 만약 잘못된 형을 함수에 넘기면 컴파일러가 이를 미리 알려주었다.
하지만 프로그래밍을 하다보면 언제나 이런 상황만 있는것은 아니다. compile 시점에는 그 형을 알 수 없고 runtime 시점이 되어야 알 수 있는 경우가 있다. Go 에서는 이를 해결하기 위해 interface{} 형을 제공하는데, any type 이라고 생각하면 된다. 그래서 처음에 언급한 함수 시그니처 walk(x interface{}, fn func(string))에서 x는 어떤 값에도 대응될 수 있다.
그렇다면 그냥 모든곳에 interface형을 작성하면 유연한 함수가 되지 않을까?
interface{} 형을 인자로 갖는 함수의 사용자는 type 안정성을 잃게된다. 만약 함수에 string형 Foo.bar를 전달하려고 했는데 int형 Foo.baz를 잘못 전달하면 어떻게 될까? 컴파일러가 이런 실수를 미리 탐지할 수 없다. 그래서 함수에 전달할 수 있는 항목을 알 수 없게 된다.
이런 함수의 작성자는 전달된 것들을 검사할 수 있어야 하고, 형이 무엇인지 그걸로 무엇을 할 수 있는지를 알아낼 수 있어야 한다. reflection을 사용하면 이것이 가능해진다. 하지만 다루기가 상당히 힘들고 가독성이 좋지 않으며 일반적으로는 runtime에 확인하는 과정이 추가되기 때문에 성능이 좋지 않다.
결국 reflection은 정말로 필요할 때 써야한다.
만약 다형성이 요구된다면 interface(any type을 나타내는 interface{}가 아닌 구현체가 구현해야하는 정의들을 모은 interface)를 사용하도록 고려해야한다. 그래야 사용자들이 함수가 동작하는데 필요한 메소드들을 구현한 다양한 구현체들로 해당 함수를 사용할 수 있다.
언제나처럼 점진적 접근법을 통해서 필요한 새로운 각 기능에 대한 test를 작성하고 리팩토링하는 과정을 거쳐보자.
- Struct
요구사항대로라면 string field를 갖는 x로 struct를 호출해야한다. 그리고 함수 fn을 통해서 string이 발견될때 이를 관찰(spy)할 수 있다.
func TestWalk(t *testing.T) {
expected := "Chris"
var got []string
x := struct {
Name string
}{expected}
walk(x, func(input string) {
got = append(got, input)
})
if len(got) != 1 {
t.Errorf("wrong number of function calls, got %d want %d", len(got), 1)
}
}
walk에 의해 fn으로 전달된 string들을 저장하는 string의 slice(got)을 저장한다. 이전 챕터에서 종종 이런 요구사항을 만족하기 위해 함수나 메소드를 호출을 감시하기 위한 spy를 만들었지만 이번에는 got을 모으기위한 익명함수 fn만 전달한다.
Name field를 갖는 익명 struct를 사용하였다. 마지막으로 x와 spy로 walk를 호출해서 got의 길이만 확인하고 있다.
func walk(x interface{}, fn func(input string)) {
}
....
func walk(x interface{}, fn func(input string)) {
fn("I still can't believe South Korea beat Germany 2-0 to put them last in their group")
}
빈 내용의 walk로 실행하면 fail이 나는것을 확인할 수 있을것이다. 그리고 pass하기 위해 fn을 호출해준다. 코드를 보면 알 수 있듯이 fn이 무엇으로 호출되는지에 대한 더 자세한 단언문(assertion)을 작성할 필요가 있다.
if got[0] != expected {
t.Errorf("got %q, want %q", got[0], expected)
}
단언문을 위와 같이 고쳐준다. 이를 통과하기 위해 walk를 수정한다.
func walk(x interface{}, fn func(input string)) {
val := reflect.ValueOf(x)
field := val.Field(0)
fn(field.String())
}
현재 코드는 매우 불안정하고 세심하게 고려를 하지 못한 상태지만 언제나 그랬듯이 "red" 상태일 때 우리의 목적은 가능한 가장 적은 코드를 작성해서 통과하는것이다.
위의 코드에서는 x를 살펴보고 속성을 살펴보기 위해 reflection을 사용한다. reflection package는 ValueOf 함수를 갖고 있는데 해당 변수의 Value형을 반환한다. Value형을 이용하묜 field와 포함한 값을 검사할 수 있다.
그런 다음 전달된 값에 대해 매우 낙관적인 가정을 하고 있는데, 첫번째 Field 하나만 살펴보고 있으며 panic을 일으키는 field가 전혀없다고 가정하고 있다. 그리고 String() 을 호출하였는데 이는 값이 문자열이라는 가정하에 한 행동이며 string형이 아니라면 잘못된 코드라는것을 인지하고 있을것이다.
현재 작성된 코드가 매우 간단한 경우만 test 하고 있지만 많은 단점이 있다는것을 알 수 있다.
또한 여러 test를 할 수 있도록 변경해야 한다. 이를 위해 테이블 기반으로 test를 할 수 있도록 코드를 리팩토링 해보자. 이렇게 하면 새로운 시나리오들이 계속 생겨도 쉽게 test를 추가할 수 있다.
func TestWalk(t *testing.T) {
cases := []struct{
Name string
Input interface{}
ExpectedCalls []string
} {
{
"Struct with one string field",
struct {
Name string
}{ "Chris"},
[]string{"Chris"},
},
}
for _, test := range cases {
t.Run(test.Name, func(t *testing.T) {
var got []string
walk(test.Input, func(input string) {
got = append(got, input)
})
if !reflect.DeepEqual(got, test.ExpectedCalls) {
t.Errorf("got %v, want %v", got, test.ExpectedCalls)
}
})
}
}
새로운 시나리오를 쉽게 추가할 수 있고 1개의 문자열 field 이상의 경우를 test 할 수 있게되었다. 아래처럼 2개의 Field를 검사하는 경우를 추가하자.
{
"Struct with two string fields",
struct {
Name string
City string
}{"Chris", "London"},
[]string{"Chris", "London"},
}
walk 함수도 여러 값을 체크할 수 있도록 변경해준다.
func walk(x interface{}, fn func(input string)) {
val := reflect.ValueOf(x)
for i:=0; i<val.NumField(); i++ {
field := val.Field(i)
fn(field.String())
}
}
ValueOf가 반환한 값을 담고 있는 val은 NumField 메소드를 갖고 있는데 field의 개수를 반환한다. 이로서 field들을 순회할 수 있게 되고 fn을 호출하여 test를 통과할 수 있게되었다.
리팩토링할 사항은 크게 보이지 않는다. 다만 walk 함수를 보면 한 가지 단점이 보이는데 모든 field가 string이라고 가정하고 있다는것이다. 이를 위해 string이 아닌 형을 포함한 test를 하나 추가해주자.
{
"Struct with non string field",
struct {
Name string
Age int
}{"Chris", 33},
[]string{"Chris"},
},
이 test를 통과하기 위해 형이 string인지를 검사해야하는데 Kind 메소드를 이용해서 확인할 수 있다.
func walk(x interface{}, fn func(input string)) {
val := reflect.ValueOf(x)
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if field.Kind() == reflect.String {
fn(field.String())
}
}
}
- Nested struct
이번에도 리팩토링할 사항이 눈에 띄지는 않지만 누락된 경우가 있어보인다. 현재까지는 struct 가 중첩되는 경우가 없다고 가정하고 있지만 중첩될경우 모든 string형을 추출해내지 못한다.
{
"Nested fields",
struct {
Name string
Profile struct {
Age int
City string
}
}{"Chris", struct {
Age int
City string
}{33, "London"}},
[]string{"Chris", "London"},
},
위의 코드에서는 익명 struct 문법을 사용하여 형을 정의하였다. 하지만 내부에 이렇게 익명 struct 구문을 사용하게 되면 코드가 지저분해진다. struct형을 정의하고 이를 참조하도록 변경하자. 이렇게 되면 test를 위한 코드중 일부가 test 외부에 있게 되지만 읽는 사람이 struct의 구조를 쉽게 파악할 수 있게 되는 장점이 있다.
type Person struct {
Name string
Profile Profile
}
type Profile struct {
Age int
City string
}
test 파일어딘가에 위의 struct를 선언해주도록 하자.
{
"Nested fields",
Person{
"Chris",
Profile{33, "London"},
},
[]string{"Chris", "London"},
},
처음 익명 struct보다 훨씬 가독성이 좋아진것을 알 수 있다. 이제 이런 경우에 통과하도록 하는 코드를 작성해보자.
func walk(x interface{}, fn func(input string)) {
val := reflect.ValueOf(x)
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if field.Kind() == reflect.String {
fn(field.String())
}
if field.Kind() == reflect.Struct {
walk(field.Interface(), fn)
}
}
}
해결하는법이 크게 어렵지는 않다. 또 다시 Kind 메소드를 사용해서 struct이면 walk를 재귀호출 해주면 된다.
func walk(x interface{}, fn func(input string)) {
val := reflect.ValueOf(x)
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
하나의 같은 값을 비교할때에 일반적으로 switch형으로 리팩토링해주면 가독성이 좋아지고 확장성이 좋아진다.
- Pointer
만약 넘어온 struct의 값이 pointer라면 어떻게 될까?
{
"Pointers to things",
&Person{
"Chris",
Profile{33, "London"},
},
[]string{"Chris", "London"},
},
위와 같이 test case를 추가해주자.
func walk(x interface{}, fn func(input string)) {
val := reflect.ValueOf(x)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
pointer Value에 대해서는 NumField를 사용할 수 없다. Elem()을 사용해서 본격적인 처리를 거치기 전에 value를 추출할 필요가 있다.
test를 통과했다면 interface{} 형을 인자로 받아서 이를 reflect.Value로 추출하는 책임을 캡슐화하도록 리팩토링해보자.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
func getValue(x interface{}) reflect.Value {
val := reflect.ValueOf(x)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
return val
}
단순히 코드량 자체는 많아졌지만 추상화 수준의 측면에 대해서는 합리적인 코드일것이다. x로 부터 reflect.Value를 얻어서 검사하는 과정을 신경쓰지 않아도 도되며, field를 순회하면서 형에 따라서 수행되어야 하는 작업을 완료하고 있다.
- Slice & Array
이제 slice인 경우도 고려해보자.
{
"Slices",
[]Profile {
{33, "London"},
{34, "Reykjavík"},
},
[]string{"London", "Reykjavík"},
},
앞에서의 pointer의 경우와 비슷하다. reflect.Value에서 NumField를 호출하려고 하면 안되는데 struct처럼 하나가 아니기 때문이다.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
if val.Kind() == reflect.Slice {
for i:=0; i< val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
return
}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
동작은 하지만 손볼곳이 좀 있다. 조금만 추상적으로 생각해보면 우리가 원하는 작업은 둘 중 하나라는것을 알 수 있다. Struct의 각 Field에 대한것이거나 slice 내의 어떤 것이거나.
특정 순간의 코드는 이를 반영하고있다고 볼 수 있지만, 잘 반영하고 있다고 보기는 힘들다. 지금은 slice에 대해 확인하는 동작을 시작부분에만 하고 있고, 그렇지 않은 경우는 struct 라고 가정하고 있다.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
switch val.Kind() {
case reflect.Struct:
for i:=0; i<val.NumField(); i++ {
walk(val.Field(i).Interface(), fn)
}
case reflect.Slice:
for i:=0; i<val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
case reflect.String:
fn(val.String())
}
}
훨씬 나아진것 같다. 만약 struct 이거나 slice 이면, walk 를 호출하여 값들을 순회하고 있다. 그렇지 않은 경우 중 만약 reflect.String 이면 fn 함수를 호출해준다.
하지만 여전히 리팩토링할 사안이 남아있다. field 와 value를 순회하면서 walk를 호출하는 동작이 반복되고 있는데 사실 개념적으로는 같은 동작이라고 할 수 있다.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
numberOfValues := 0
var getField func(int) reflect.Value
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Struct:
numberOfValues = val.NumField()
getField = val.Field
case reflect.Slice:
numberOfValues = val.Len()
getField = val.Index
}
for i:=0; i< numberOfValues; i++ {
walk(getField(i).Interface(), fn)
}
}
value가 reflect.String이면 fn만 호출해주면 된다. 만약 그렇지 않으면 형에 따라 switch에서 2 가지 항목을 추출한다. 얼마나 많은 field들이 있는지에 대한 numOfValues와 어떻게 Value를 추출할것인지(Field 또는 Index)에 대한 동작에 대해서 동적으로 할당해준다.
이 2 가지만 동적으로 결정해주면 numberOfValus를 통해 순회하면서 getField 함수의 결과를 가지고 walk 함수를 호출해줄 수 있다.
{
"Arrays",
[2]Profile {
{33, "London"},
{34, "Reykjavík"},
},
[]string{"London", "Reykjavík"},
},
slice가 아닌 array도 똑같이 다룰 수 있으므로 Slice 처리하는 case에 Array 항목을 추가해주기만 하면 된다.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
numberOfValues := 0
var getField func(int) reflect.Value
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Struct:
numberOfValues = val.NumField()
getField = val.Field
case reflect.Slice, reflect.Array:
numberOfValues = val.Len()
getField = val.Index
}
for i:=0; i< numberOfValues; i++ {
walk(getField(i).Interface(), fn)
}
}
- Map
다음으로는 map을 다루어본다.
{
"Maps",
map[string]string{
"Foo": "Bar",
"Baz": "Boz",
},
[]string{"Bar", "Boz"},
},
추상적으로 생각해보자면 map은 struct와 비슷한데, 단지 컴파일 시점에 key들을 알지 못한다는점만 다르다.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
numberOfValues := 0
var getField func(int) reflect.Value
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Struct:
numberOfValues = val.NumField()
getField = val.Field
case reflect.Slice, reflect.Array:
numberOfValues = val.Len()
getField = val.Index
case reflect.Map:
for _, key := range val.MapKeys() {
walk(val.MapIndex(key).Interface(), fn)
}
}
for i:=0; i< numberOfValues; i++ {
walk(getField(i).Interface(), fn)
}
}
하단의 for문과 합쳐주면 좋겠지만 설계상 index로 map에서 값을 추출할 수 없다. key에 의해서만 가능하므로 Map인 경우에는 우리의 추상화가 깨지는부분이라고 할 수 있다.
map의 경우를 추가하기전까지는 추상화를 깔끔하게 유지했었는데 이번에는 그런 깔끔함이 깨져버렸다. 단계를 거듭하면서 실수를 할 수 있는게 리팩토링이며 TDD의 중요한점은 우리에게 이런 시도를 해볼 수 있는 자유를 보장해준다는 점이다.
조금만 이전 단계의 코드로 되돌려보자.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
walkValue := func(value reflect.Value) {
walk(value.Interface(), fn)
}
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Struct:
for i := 0; i< val.NumField(); i++ {
walkValue(val.Field(i))
}
case reflect.Slice, reflect.Array:
for i:= 0; i<val.Len(); i++ {
walkValue(val.Index(i))
}
case reflect.Map:
for _, key := range val.MapKeys() {
walkValue(val.MapIndex(key))
}
}
}
walkValue 함수를 추가하였다. 이를 통해 switch 함수내에서 walk 함수에 대한 호출 중복을 제거함으로써 val에 대해 reflect.Value들만 추출할 수 있도록 하였다.
Go에서는 map에 대해 순서를 보장해주지 않는다. 그래서 test를 돌리다보면 가끔 실패할 수 있는데 우리가 fn을 호출해서 검사할때에는 순서에 영향을 받기 때문이다.
이를 고치기 위해 map에 대한 선언문을 별도의 test로 옮겨서 순서에 영향을 받지 않도록 해야한다.
t.Run("with maps", func(t *testing.T) {
aMap := map[string]string{
"Foo": "Bar",
"Baz": "Boz",
}
var got []string
walk(aMap, func(input string) {
got = append(got, input)
})
assertContains(t, got, "Bar")
assertContains(t, got, "Boz")
})
아래처럼 assertContains를 작성해준다.
func assertContains(t testing.TB, haystack []string, needle string) {
t.Helper()
contains := false
for _, x := range haystack {
if x == needle {
contains = true
}
}
if !contains {
t.Errorf("expected %+v to contain %q but it didn't", haystack, needle)
}
}
- Chan
다음으로는 chan형을 다룬다.
t.Run("with channels", func(t *testing.T) {
aChannel := make(chan Profile)
go func() {
aChannel <- Profile{33, "Berlin"}
aChannel <- Profile{34, "Katowice"}
close(aChannel)
}()
var got []string
want := []string{"Berlin", "Katowice"}
walk(aChannel, func(input string) {
got = append(got, input)
})
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
이를 통과하도록 walk를 수정해보자.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
walkValue := func(value reflect.Value) {
walk(value.Interface(), fn)
}
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
walkValue(val.Field(i))
}
case reflect.Slice, reflect.Array:
for i := 0; i < val.Len(); i++ {
walkValue(val.Index(i))
}
case reflect.Map:
for _, key := range val.MapKeys() {
walkValue(val.MapIndex(key))
}
case reflect.Chan:
for v, ok := val.Recv(); ok; v, ok = val.Recv() {
walkValue(v)
}
}
}
Recv() 함수를 이용하면 channel이 닫히기전까지 channel로 보냈던 값들을 순회할 수 있다.
- Func
다음으로는 func를 다룬다.
t.Run("with function", func(t *testing.T) {
aFunction := func() (Profile, Profile) {
return Profile{33, "Berlin"}, Profile{34, "Katowice"}
}
var got []string
want := []string{"Berlin", "Katowice"}
walk(aFunction, func(input string) {
got = append(got, input)
})
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
})
인자가 있는 함수들을 다루는것은 이 시나리오에서 의미가 없으므로 값들을 반환하는 경우에 대해서만 다루어본다.
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
walkValue := func(value reflect.Value) {
walk(value.Interface(), fn)
}
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
walkValue(val.Field(i))
}
case reflect.Slice, reflect.Array:
for i := 0; i < val.Len(); i++ {
walkValue(val.Index(i))
}
case reflect.Map:
for _, key := range val.MapKeys() {
walkValue(val.MapIndex(key))
}
case reflect.Chan:
for v, ok := val.Recv(); ok; v, ok = val.Recv() {
walkValue(v)
}
case reflect.Func:
valFnResult := val.Call(nil)
for _, res := range valFnResult {
walkValue(res)
}
}
}
'Language > Go' 카테고리의 다른 글
Go - Context (0) | 2022.02.27 |
---|---|
Go - Sync (0) | 2022.01.20 |
Go - select (0) | 2022.01.12 |
Go - Concurrency (0) | 2022.01.04 |
Go - mocking (0) | 2022.01.02 |
댓글