ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 12장 리플렉션
    카테고리 없음 2019. 12. 1. 21:51
    The Go Programming language

    개요

    리플렉션(reflection)은 컴파일 시 미리 알 수 없는 타입에 대해 런타임에 변수를 갱신하고 값을 조사하며 메소드를 호출하고 표현 방식에 따른 고유 작업을 적용한다.

    리플렉션은 타입 자체를 일급 클래스 값으로 취급할 수 있다.

    12.1 왜 리플렉션인가?

    알려지지 않은 타입 값의 표현을 검사하는 방법 없이는 바로 문제에 부딪치게 된다. 이때 리플렉션이 필요하다.

    12.2 reflect.Type과 reflect.Value

    리플렉션은 reflect 패키지로 제공된다. 이 패키지에는 두 가지 중요한 타입인 Type과 Value가 정의돼 있다. Type은 Go 타입을 나타낸다. Type은 타입을 식별하거나 구조체의 필드나 함수의 파라미터와 같은 구성 요소를 조사하기 위한 다수의 메소드가 있는 인터페이스이다. reflect.Type의 유일한 규현은 타입 서술자(descriptor 7.5절)로 인터페이스 값의 동적 타입을 식별하는 개체다.

    reflect.TypeOf 함수는 모든 interface{}를 받고 그 동적 타입을 reflect.Type으로 반환한다.

    t := reflect.TypeOf(3) // a reflect.Type 
    fmt.Println(t.String()) // "int" 
    fmt.Println(t) // "int"

    위의 TypeOf(3) 호출은 파라미터 interface{}에 값으로 3을 할당한다. 동적 타입은 피연산자의 타입(int)이며, 동적 값은 피연산자의 값(3)이다.

    reflect.TypeOf는 인터페이스 값의 동적 타입을 반환하므로 항상 구상 타입을 반환하게 된다.

     

    reflect.Value는 어떤 타입의 값도 저장할 수 있다. reflect.ValueOf 함수는 모든 interface{}를 받고 인터페이스의 동적 값을 포함하는 reflect.Value를 반환한다. reflect.ValueOf는 reflect.TypeOf와 마찬가지로 항상 구상 값을 반환하지만 reflect.Value에는 인터페이스 값도 저장할 수 있다.

    v := reflect.ValueOf(3) // a reflect.Value
    fmt.Println(v) // "3"
    fmt.Printf("%v\n", v) // "3"
    fmt.Println(v.String())// NOTE: "<int Value>"

    reflect.Value는 reflect.Type처럼 fmt.Stringer를 충족하지만 Value가 문자열이 아닐 때에는 String 메소드의 결과는 타입만 보여준다. 대신 reflect.Value를 특별히 취급하는 fmt 패키지의 %v 포매터를 사용하라.

     

    reflect.ValueOf의 역연산은 reflect.Value.Interface 메소드이다. 이 메소드는 reflect.Value와 동일한 구상 값을 갖는 interface{}를 반환한다.

    reflect.Value의 제로 값은 Invalid 타입이다.

    12.3 재귀 값을 출력하는 Display

    fmt.Sprintf를 있는 그대로 복사하는 대신 임의의 복합 값 x가 주어졌을 때 값의 전체 구조를 각 원소의 찾은 경로와 함께 출력하는 디버깅용 도우미 함수 Display를 만든다.

    func display(path string, v reflect.Value) { 
    	switch v.Kind() {
    	case reflect.Invalid:
    		fmt.Printf("%s = invalid\n", path) 
        case reflect.Slice, reflect.Array: 
        	for i := 0; i < v.Len(); i++ {
    			display(fmt.Sprintf("%s[%d]", path, i), v.Index(i)) 
            }
    	case reflect.Struct:
    		for i := 0; i < v.NumField(); i++ {
    			fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
    			display(fieldPath, v.Field(i)) }
    	case reflect.Map:
    		for _, key := range v.MapKeys() {
    			display(fmt.Sprintf("%s[%s]", path, formatAtom(key)), v.MapIndex(key))
    		}
    	case reflect.Ptr:
    		if v.IsNil() {
    			fmt.Printf("%s = nil\n", path)
    		} else {
    			display(fmt.Sprintf("(*%s)", path), v.Elem())
    		}
    	case reflect.Interface:
    		if v.IsNil() {
    			fmt.Printf("%s = nil\n", path)
    		} else {
    			fmt.Printf("%s.type = %s\n", path, v.Elem().Type()) 
                display(path+".value", v.Elem())
            }
    	default: // basic types, channels, funcs
    		fmt.Printf("%s = %s\n", path, formatAtom(v)) 
        }
    }
    • 슬라이스와 배열

    v.Len() 메소드는 슬라이스나 배열 값의 원소 개수를 반환한다.

    v.Index(i)는 reflect.Value인 인덱스 i의 원소를 반환한다. Index 메소드는 Slice, Array, String에 호출할 수 있지만 그 외의 경우에는 패닉을 일으킨다.

    • 구조체

    v.NumField() 메소드는 구조체의 필드 수를 보고한다.

    v.Field(i)는 i 번째 필드 값을 reflect.Value로 반환한다.

    v.MapKeys() 메소드는 맵 키마다 reflect.Value의 슬라이스를 하나씩 반환한다.

    v.MapIndex(key)는 key에 해당하는 값을 반환한다.

    • 포인터

    v.Elem() 메소드는 포인터로 지정된 변수를 reflect.Value로 반환한다.

    • 인터페이스

    동적 값을 v.Elem()으로 얻어서 타입과 값을 출력한다.

     

    리플렉션은 익스포트되지 않은 필드도 볼 수 있다.

    12.4 예제: S-표현식 인코딩

    Display는 구조화된 자료를 표시할 수 있는 디버깅 루틴이지만 임의의 Go 객체를 프로세스간 통신에 접합한 이동성이 있는 메시지로 인코딩하거나 직렬화하기에는 부족하다.

     

    S-표현식은 리스프(Lisp)의 문법이며 JSON, XML, ASN.1 표기법과는 달리 여러 표준화 시도와 수많은 구현에도 불구하고 아직까지 범용적인 정의가 없으므로 Go의 표준 라이브러리에서 지원하지 않는다.

     

    S-표현식용 Marshal 함수는 인코딩을 수행한다.

    12.5 reflect.Value로 변수 설정

    여기서는 리플렉션으로 값을 변경하는 것을 다룬다.

    일부는 주소를 참조해서 변경할 수 있고, 일부는 그렇지 않다.

    x := 2 // value type   variable?
    a := reflect.ValueOf(2) // 2 int    no
    b := reflect.ValueOf(x) // 2 int    no
    c := reflect.ValueOf(&x) // &x *int   no
    d := c.Elem() // 2 int yes (x)

    a, b는 단지 정수 2의 복사본이다.

    c의 값도 포인터 값 &x의 복사본이므로 주소를 참조할 수 없다.

    reflect.ValueOf(x)에서 반환된 reflect.Value는 주소로 참조할 수 없지만 reflect.ValueOf(&x).Elem()을 호출해 모든 변수 x에 대해 주소로 참조할 수 있는 Value를 얻을 수 있다.

     

    주소로 참조할 수 있는 reflect.Value에서 변수를 얻는 데에는 3단계가 필요하다. 1단계: Addr()을 호출해 변수의 포인터를 갖는 Value를 얻는다. 2단계: 이 Value의 interface()를 호출해 포인터를 갖는 interface{}를 얻는다. 3단계: 변수의 타입을 알고 있다면 타입 단언을 통해 인터페이스의 내용을 일반 포인터로 추출할 수 있다. 그 후 포인터를 통해 변수를 변경할 수 있다.

    x := 2
    d := reflect.ValueOf(&x).Elem() // d refers to the variable x 
    px := d.Addr().Interface().(*int) // px := &x
    *px = 3 // x = 3
    fmt.Println(x)// "3"

    또는 주소로 참조할 수 있는 reflect.Value가 참조하는 변수는 포인터를 사용하지 않고 reflect.Value.Set 메소드를 호출해 직접 변경할 수 있다.

    d.Set(reflect.ValueOf(4)) 
    fmt.Println(x) // "4"

    12.6 예제: S-표현식 디코딩

    S-표현식용 Unmarshal 함수는 디코딩을 수행한다.

    12.7 구조체 필드 태그 접근

    여기서는 HTTP 처리기(7.7절)를 더 편리하게 만들기 위해 구조체 필드 태그를 사용하는 도우미 함수 params.Unpack을 정의한다.

    12.8 타입의 메소드 표시

    여기서는 reflect.Type으로 임의 값의 타입을 출력하고 메소드를 열거하는 것이다.

     

    reflect.Type과 reflect.Value에는 Method 메소드가 있다. t.Method(i) 호출은 단일 메소드의 이름과 타입을 설명하는 구조체 타입 reflect.Method의 인스턴스를 반환한다. 각 v.Method(i) 호출은 수신자에 묶인 메소드인 메소드 값(6.4절)을 표현하는 reflect.Value를 반환한다.

     

    reflect.Value.Call 메소드를 사용하면 Func에 Value를 호출할 수 있다.

    12.9 주의 사항

    리플렉션 API는 이보다 많다.

    3가지로 이유로 리플렉션을 신중히 사용해야 한다.

    첫 번째 이유는 리플렉션에 기반한 코드가 깨지기 쉽다. 컴파일러는 빌드시 실수를 보고하는 반면 리플렉션 오류는 실행시 패닉으로 보고된다.

    두 번째 이유는 타입이 문서화의 형태 역할을 하며 리플렉션에서 동작은 정적 타입 검사의 대상이 될 수 없어서 리플레션에 과도하게 의존하는 코드는 이해하기 어렵다.

    세 번째 이유는 리플렉션에 기반을 둔 함수가 일부 타입에 특화된 코드보다 수 배 느리다.

Designed by Tistory.