우리는 Go에서 함수가 string, int 그리고 우리의 자료형인 BackAccount와 같이 알려진 자료형으로 동작한다는 측면에서 type-safety의 편리성을 느껴왔다.
이것은 우리가 쉽게 문서화 할 수 있다는 것과 만약 함수에 잘못된 자료형을 전달하는 경우, 컴파일러가 이를 알아 낼 것임을 의미한다.
컴파일 시 자료형에 대해 알지 못하는 함수를 작성하려는 상황을 접할 수 있다.
Go는 이 문제를 해결하기 위해 모든 자료형이라 생각할 수 있는 interface{}라는 자료형을 제공한다.
따라서, walk(x interface{}, fn func(string))은 x로 어떠한 값도 받을 수 있다.
그렇다면 모든 것에 interface를 사용하고 정말 유연한 함수를 갖는 건 어떨까?
interface를 사용하는 함수의 사용자는 type-safery를 잃게 된다. 만약 string 형인 Foo.bar를 함수에 전달하도록 의도했지만 int형의 Foo.baz가 전달됐다면? 컴파일러는 그 실수를 알려줄 수 없을 것이다. 또한 함수에 무엇이 잘되어야 하는지도 알 수 없다. 예시로 함수가 UserService를 수용한다는 걸 아는 것은 매우 도움이 된다.
함수의 작성자로서, 전달될 어떠한 것에 대해 검사할 수 있어야 하며 그 자료형은 무엇인지, 그것으로 무엇을 할 수 있는지를 알아야 한다. 이것을 위해 reflection을 이용한다. 이는 상당히 익숙치 않고 읽기 어려울 수 있으며, 일반적으로 성능이 저하된다(런타임에 검사를 해야함).
간략히 말해, 정말 필요할 때에 refection을 사용한다.
만약 다형성을 가진 함수(polymorphic functions)를 원한다면, 인터페이스(interface가 아님)를 중심으로 설계할 수 있는지 고려한다. 그러면 사용자는 그 함수가 동작하는데 필요한 방법을 구현 할 때 여러 자료형을 통해 함수를 사용할 수 있다.
우리의 함수는 다른 많은 것들과 함게 동작해야 할 것이다. 항상 그랬듯, 우리는 우리가 지원하고자 하는 새로운 것에 대한 테스트를 작성하고 끝날 때까지 리팩토링하는 반복적인 접근법을 취할 것이다.
테스트부터 작성하기
우리는 내부에 string 필드(x)를 갖는 구조체와 함께 함수를 호출할 것이다. 그러면 전달된 함수(fn)에서 그것이 호출되는 지 확인할 수 있다.
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에 들어오는 문자열을 담는 문자열 슬라이스를 저장하고자 한다. 이전 장에서는 함수/메소드의 호출부에 전용 자료형을 만들었지만, 이 경우에는 단지 got에 접근하는 익명 함수 fn을 전달한다.
우리는 가장 단순한 방법을 위해 string 자료형인 Name을 갖는 익명 구조체를 사용한다.
마지막으로, x와 함께 walk를 호출하고 got의 길이를 확인한다. 그리고 우리가 아주 기본적인 일을 하게 될 때, assertions에 대해 조금 더 자세히 알아본다.
=== RUN TestWalk
--- FAIL: TestWalk (0.00s)
reflection_test.go:23: got 'I still can't believe South Korea beat Germany 2-0 to put them last in their group', want 'Chris'
FAIL
테스트를 통과하는 최소한의 코드 작성하기
func walk(x interface{}, fn func(input string)) {
val := reflect.ValueOf(x)
field := val.Field(0)
fn(field.String())
}
이 코드는 매우 위험하고 단순하지만, 우리가 "빨간색"(테스트 실패)에 있을 때 우리의 목표는 가능한 최소한의 코드를 작성하는 것임을 기억한다. 그런 다음 우리의 우려를 해결하기 위해 더 많은 테스트를 작성한다.
우리는 x와 그 속성을 알아보기 위해 reflection을 이용한다.
reflect 패키지는 주어진 변수의 값을 전달하는 ValueOf함수를 갖는다. 이것은 우리에게 값을 알아볼 방법을 제공하고 우리가 그 다음 줄에 사용한 것처럼 그 값의 필드까지도 포함한다.
그런 다음 전달된 값에 대한 매우 낙관적인 가정을 한다.
첫번째 필드를 찾아보고, panic을 일으킬 필드는 없을지도 모른다.
그런 다음, 문자열을 기본값으로 전달하는 String()을 호출하고 만약 해당 필드가 문자열이 아닌 다른 값이라면 문제가 될 것임을 안다.
리팩터링 하기
우리의 코드가 단순한 케이스에서는 통과하지만 많은 단점을 가지고 있다는 것을 안다.
우리는 여러 다른 값을 전달하는 테스트를 작성하고 fn과 함께 호출되는 문자열 집합을 확인할 것이다.
우리는 새로운 시나리오를 더 쉽게 테스트하기 위해 테스트를 표 기반 테스트로 리팩토링해야 한다.
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)
}
})
}
}
이제 우리는 하나 이상의 문자열 필드를 가질 때 어떤 일이 일어나는 지에 대한 시나리오를 쉽게 추가할 수 있다.
테스트부터 작성하기
다음의 시나리오를 cases에 추가한다.
{
"Struct with two string fields",
struct {
Name string
City string
}{"Chris", "London"},
[]string{"Chris", "London"},
}
다음 시나리오는 만약 struct가 "flat" 하지 않은 경우이다. 다른 말로, 만약 struct가 nested 필드를 갖는다면 어떻게 되는지이다.
테스트부터 작성하기
우리는 자료형을 임시방편으로 선언하기 위해 익명 구조체 구문을 사용해왔고 다음과 같이 계속할 수 있다.
{
"Nested fields",
struct {
Name string
Profile struct {
Age int
City string
}
}{"Chris", struct {
Age int
City string
}{33, "London"}},
[]string{"Chris", "London"},
},
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를 통해 다시 한번 검사하고 만약 그것이 구조체 라면 우리는 단지 내부에서 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구문으로 변경하는 것이 가독성과 확장성을 높일 수 있다.
=== RUN TestWalk/Pointers_to_things
panic: reflect: call of reflect.Value.NumField on ptr Value [recovered]
panic: reflect: call of reflect.Value.NumField on ptr Value
테스트를 통과하는 최소한의 코드 작성하기
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)
}
}
}
포인터인 값에서는 NumField를 사용할 수 없다. 우리는 그전에 드러나지 않은 값을 추출할 필요가 있고 그것은 Elem()을 통해 할 수 있다.
리팩터링
이제 주어진 함수로 주어진 interface{}로부터 refect.Value를 추출하는 기능을 encapsulate 해보자.
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를 얻고 검사할 수 있지만, 나는 그것이 어떻게 되는지 신경쓰지 않아도 된다.
=== RUN TestWalk/Slices
panic: reflect: call of reflect.Value.NumField on slice Value [recovered]
panic: reflect: call of reflect.Value.NumField on slice Value
테스트를 실행할 최소한의 코드를 작성하고 테스트 실패 결과를 확인하기
이것은 이전의 포인터 시나리오와 비슷하다. 우리는 reflect.Value에서 NumField를 호출 하려하지만, 그것은 구조체가 아니기 때문에 값이 없다.
테스트를 통과하는 최소한의 코드 작성하기
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)
}
}
}
리팩터링
이 코드는 작동하지만 조금 지저분하다. 그래도 작동하는 코드가 있으니 편안하게 우리가 좋아하는 방식으로 손볼 수 있다.
조금 추상적으로 생각한다면, 우리는 두 경우 모두에서 walk를 호출 하고 싶을 것이다.
구조체 내부의 각각의 필드
슬라이스 내부의 각각의 무언가
현재 우리의 코드는 그렇게 동작하지만, 제대로 reflect하고 있지는 않다. 그래서 그것이 슬라이스(코드의 남은 실행을 멈출 수 있는 return이 있는)인지를 처음에 검사하고 슬라이스가 아니라면 구조체라고 가정한다.
이제 다시 코드를 다시 수정해서 타입을 먼저 확인하고 작업을 진행해본다.
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())
}
}
훨씬 좋아보인다. 만약 구조체 혹은 슬라이스라면 우리는 각각 walk를 호출하며 그 값을 순회한다. 그렇지 않고 만약 relect.String이라면 fn을 호출하면 된다.
아직도 더 개선할 부분이 있어보인다. 필드/값을 순회하는 연산을 반복적으로 하고 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)
}
}
만약 값이 reflect.String이라면 평소처럼 그냥 fn을 호출한다.
그렇지 않다면, switch를 통해 타입에 의존한 두 가지 것을 추출한다.
몇 개의 필드가 있는지
어떻게 값(필드 또는 인덱스)을 추출할 것인지
이것을 정의하게되면 우리는 numberOfValues만큼 순회하며 getField함수의 결과와 함께 walk를 호출할 수 있다.
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)
}
}
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() {
walk(v.Interface(), fn)
}
}
}
인자값을 갖는 함수는 이 시나리오 상 알맞지 않다고 보인다. 하지만 우리는 임의의 리턴값도 허용해야 한다.
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() {
walk(v.Interface(), fn)
}
case reflect.Func:
valFnResult := val.Call(nil)
for _, res := range valFnResult {
walk(res.Interface(), fn)
}
}
}
정리
reflect패키지의 몇가지 개념을 설명했다.
임의의 데이터 구조를 살펴보기 위해 재귀를 사용했다.
나쁜 리팩토링을 경험했지만 이에 대해 크게 당황하지 않았다. 테스트를 반복적으로 하는 것은 그리 큰 일이 아니다.