티스토리 뷰
개요
reflection 이라는 개념이 처음에는 참 어려웠다.
Rob Pike 의 글을 따라가며 좀더 체계적으로 접근하며 이해해본다.
- 링크: https://blog.golang.org/laws-of-reflection
- 번역본 링크: https://johngrib.github.io/wiki/golang-the-laws-of-reflection/
Types and interfaces
|
예제 링크: https://play.golang.org/p/lcztLbvaUJj
간단히 몸을 풀어가자.
MyInt 타입이 실제 int 값을 담더라도 MyInt 와 int 는 엄연히 다른 타입이다. - 따라서 두 타입의 변수간에는 할당, 비교등의 연산이 불가능하다. - type casting 을 해줘야만 연산이 가능해진다.
|
|
예제 링크: https://play.golang.org/p/TpR-X4Mt42E
인터페이스 타입은 매우 독특한 녀석이다.
1) 고정된 메쏘드들의 집합으로 표현된다. → Reader 인터페이스는 Read() 라는 메쏘드 → Writer 인터페이스는 Write() 라는 메쏘드를 가지고 있다.
2) 인터페이스의 메쏘드들을 구현하기만 했다면 어떠한 concrete type 도 담을 수 있다 → duck typing!
왼쪽 코드를 보자
1) r 변수의 타입은 io.Reader 이다. - 이 말은 r 에는 메쏘드 Read() 가 구현된 타입의 값이라면 무엇이든 넣을 수 있다는 것이다.
2) r 에 다양한 타입을 넣어본다. *os.File 타입 *bufio.Reader 타입 *bytes.Buffer 타입 → 헷갈리지는 말자. Go 는 정적 타입 (statically typed) 언어이다. 컴파일시에 타입이 정해진다는 말이다. → 따라서 r 의 타입은 io.Reader 이다.
3) 이 타입 값들이 들어간다는 것은 이 타입들의 메쏘드 중에 Read() 가 있다는 것을 의미한다. |
|
예제링크: https://play.golang.org/p/Dkjg874ooH1
interface{} 타입
empty interface type 이다. 어떤 type 이건 0 개 이상의 메쏘드를 가지고 있으니, 이 타입에는 어떤 값이든 넣을 수 있다.
심지어는 인터페이스에 저장된 값의 타입이 바뀌더라도 문제없다.
인터페이스가 다양한 타입을 담을 수 있다고 동적 타입 (dynamically typed) 언어라고 헷갈리지 말자. 빈 인터페이스를 예로 들면 다양한 타입의 값이 들어갈 수 있다는 것이지, 빈 인터페이스 타입이라는 것 자체는 변하지 않는 것이다.
그렇기에 빈 인터페이스 타입의 변수가 계속 다른 타입의 값을 받을 수 있는 것이다. |
The representation of an interface
|
예제 링크: https://play.golang.org/p/rh_dbWVLMo5
1) os.OpenFile() 의 리턴 타입은 *os.File 과 error 이다. - 이 타입은 엄청나게 많은 메쏘드를 가지고 있다.
2) 그런 tty 를 r 이라는 io.Reader 타입에 넣으면 - r 이 사용할 수 있는 메쏘드는 Read() 뿐이다.
3) 하지만 r 이 담고 있는 실제 값인 *os.File 타입의 tty 는 훨씬 많은 메쏘드를 감추고 있는거다. - 그래서 w 라는 io.Writer 타입에 r 을 io.Writer 로 type assertion 해서 넣으면 - w 는 Write() 메쏘드를 쓸 수 있는 녀석이 된다.
4) empty interface type 에 넣을 수도 있다. 이때는 별도의 type assertion 이 필요없다. |
세 가지 리플렉션의 법칙을 챙겨보며 리플렉션에 대한 개념을 단단히 세워보자
첫 번째 리플렉션 법칙
리플렉션은 인터페이스 값 → 리플렉션 오브젝트 로 간다.
기본적으로 리플렉션은 인터페이스 안에 들어있는 타입과 값을 검사하는 메커니즘이다.
|
예제 코드: https://play.golang.org/p/7ip6vIUbpnS
reflect 패키지에는 크게 두 개의 타입이 있다. Type 과 Value
reflect.TypeOf() 를 먹여주면 reflect.Type 이 리턴되고 (interface 의 type 을 가져온다고 보면 되려나) reflect.ValueOf() 를 먹여주면 reflect.Value 가 리턴된다. (interface 의 value 를 가져온다고 보면 되려나)
두 메쏘드 모두 파라미터로 interface{} 를 가진다. 따라서 무슨 값을 넣든 interface{} 로 타입 컨버젼이 일어나게 된다.
reflect.Value.Type() 은 다시 reflect.Type 인건 안비밀 |
|
예제 코드: https://play.golang.org/p/t4MzxrQiQWk
reflect.TypeOf() 를 써보자
1) x 라는 float64 값을 reflect.TypeOf(x) 에 넣어서 출력해보면 2) float64 라는 타입의 값이 제대로 찍힌다. |
|
예제 코드: https://play.golang.org/p/BLtf1rAD-6g
reflect.ValueOf() 를 먹여주면 뭐가 나올까?
v 를 출력한 값보다 코드에서는 v.String() 이 좀더 의미있는 값이랄 수 있겠다. 그냥 v 를 출력하면, fmt 패키지가 기를 쓰고 파고 들어가 진짜 값 (concrete value inside) 을 찾아내어 출력하기 때문이다
|
|
예제 코드: https://play.golang.org/p/Ya488QY6bhE
reflect.Type 과 reflect.Value 의 메쏘드들
reflect.Type 이라는 타입과 reflect.Value 라는 타입은 타입이니깐 메쏘드를 가질 수 있고, 또한 가지고 있는데 → 이 메쏘드들을 이용하면 이 녀석들을 들여다보고, 조작할 수 있다.
1) reflect.Value 의 Type() 이라는 메쏘드는 reflect.Type 을 리턴해준다. 요거 밑줄쫙 2) reflect.Type 과 reflect.Value 라는 녀석 둘다 가지고 있는 Kind() 라는 메쏘드는 → 상수값 (constant) 를 리턴해주는데 다름아닌 어떤 녀석들이 들어있는지를 알려주는 값이다. → reflect.Uint, reflect.Float64, reflect.Slice 같은 값 3) reflect.Value 는 Int(), Float() 같은 메쏘드를 가지는데 reflect.Value 안의 값을 꺼내올 수 있게 해준다. → 예제에서는 float64 라는 걸 알기에 Float() 를 이용하였다. → 예제에서 Int() 를 사용하면 panic 이 발생한다.
Settability
SetInt 나 SetFloat 같은 메쏘드들은 settability 개념을 알아야 하는데 요건 세 번째 법칙에서 살펴 보자 |
|
예제 코드: https://play.golang.org/p/wDZtDSBcDU9
Largest type
reflect.Value 의 값을 가져오거나, 값을 넣으면 getter 라 할수 있는 Int() 라면 int64로, Uint() 라면 uint64 로 setter 라 할 수 있는 SetInt() 라면 int64 로 할당된다.
→ 즉, 값을 담을 수 있는 같은 부류 중에서 제일 메모리가 큰 타입으로 먹여진다.
옆의 예제를 보아도 1) 분명히 v.Kind() 는 reflect.Uint8 인데, 2) v.Uint() 로 값을 읽어내면 uint64 이다. → 조금 생각해보면 왜 이렇게 구현되어 있는지 느낌이 오겠지만 일단 여기서는 생략 |
|
예제 코드: https://play.golang.org/p/7P3jRZF4ifw
Kind 는 내면을 본다.
요걸 유심히 보자. v 의 Kind() 는 MyInt 가 아니라 int 인게 중요한거다. 내면을 본다는 거창한 말을 썼지만 달리 말하자면 MyInt 와 int 를 구분 못한다는 말이다. |
두 번째 리플렉션 법칙
리플렉션은 리플렉션 오브젝트 → 인터페이스 값 으로도 간다.
reflection 은 반사, 반영이다. 따라서 반대로 리플렉션 오브젝트가 인터페이스 값으로 되기도 한다.
|
예제 코드: https://play.golang.org/p/wdM1YtFRCTD
Interface() 메쏘드
reflect.Value 라는 타입의 Interface() 메쏘드를 쓰면 reflect.Value 에서 interface{} 를 추출해낼 수 있다.
다시 이녀석을 type assertion 해주면 진짜 값을 얻어내게 된다.
fmt 패키지는 무시무시하게 만들어진 놈이라 알아서 concrete 값을 추출해내어 출력해준다. 심지어는 이 넘이 float64 인 경우 format 까지 맞춰 넣을 수 있다.
왜 빈 인터페이스 타입 값을 .(float64) 로 type assertion 안해줘도 될까? 빈 인터페이스 타입의 값이 concrete value 의 타입 정보를 내부에 가지고 있기 때문이다.
다시 한 번 복습하자.
reflect.Value 의 메쏘드인 Interface() 를 먹이면, static type 인 interface{} 타입. 즉, 빈 인터페이스 타입의 값이 리턴된다.
리플렉션 오브젝트 < - > 인터페이스 값 |
|
예제 코드: https://play.golang.org/p/vd6B6ppu_fB
Interface() 메쏘드의 리턴값
중복이 되겠지만 약간 장난을 쳐보았다.
1) x 는 interface{} 타입이고 2) y 는 float64 타입인 것이다.
따라서 x 는 string 인 "hello" 를 할당할 수 있지만 y 는 string 을 할당하려 했다간 컴파일 에러가 난다. |
세 번째 리플렉션 법칙
리플렉션 오브젝트를 수정하려면, 그 값은 settable 해야 한다.
세 번째 법칙은 헷갈릴 수 있겠지만 , 첫번째 원칙부터 차근히 보면 이해 못할것도 아니다.
|
예제코드: https://play.golang.org/p/F9lH1vLIYeZ
reflect.Value 의 SetFloat() 메쏘드를 써보자
v 라는 reflect.Value 에다가 v.SetFloat() 라는 메쏘드로 값을 쓰려고 했더니 패닉이 났다. - panic: reflect: reflect.flag.mustBeAssignable using unaddressable value - unaddressable value 에 값을 우겨 넣으려 해서 그렇단다
- v 는 not settable 이다. 그래서 생긴 문제이다. ➔ settability 는 reflect.Value 의 속성이다. ➔ 그런데 모든 reflect.Value 가 이 속성을 가진건 아니다.
v := reflect.ValueOf(x) 라는 코드는 원래값 x 를 copy 해서 reflect.Value 인 v 를 만들어낸 것이다. 복사한 값을 변경해준다고 원래 값이 바뀔리가 없잖아?
v.SetFloat() 는 패닉을 발생시키지만, 이게 동작하게 만든다해도, 원래 값인 x 가 변경되는 일 따위는 절대 발생하지 않는다. 그래서 패닉을 일으키게 해둔거다. 멍충이가 v.SetFloat(7.1) 을 해놓고, x 가 업데이트 되었으려니 하지 않게 만든 것이다.
reflect.Value 의 CanSet() 메쏘드를 써보자
v.CanSet() 이라고 물어보면 settable 할지 여부를 알 수 있다.
1. settability 는 addressability 랑 비슷하다. 어느 메모리에 저장되어 있는지 알 수 있다는 것 정도? 하지만 좀더 엄격하다
2. 원래 변수 (= 실제 값의 저장 공간)에서 우리는 reflection object 를 생성할 수 있다. 복제! - reflect.ValueOf() 또는 reflect.TypeOf() 같은 걸로 - reflect.Value, reflect.Type 을 만들 수 있다는 것이다. → settability 는 원래 변수를 바꿀 수 있는가 여부인 것이다. |
사실
함수를 생각해보면 별
이상한 것도
아니다.
- f(x) 로 값을 함수에 전달하면, 함수내에서 argument 를 변경한다고 하여 원래 값이 변경될 리 없지만
- f(&x) 로 레퍼런스 (=포인터) 를 전달하면 원래 값인 x 를 변경할 수 있는 것이다.
|
예제 코드: https://play.golang.org/p/tZ72mxpWXg2
실습을 해보자.
변수 x 가 있다고 할때 변수의 reflection 을 만들어서 변수 값을 바꿔보자. → 이 시점 궁금한건 이걸 어따쓰지? → 타입을 모르는 녀석을 가져와서 값을 변경하려면?
1) float64 타입의 x 를 선언하고 초기값을 3.4로 넣었다. 2) reflect.ValueOf(&x) 를 통해서 refelct.Value 인 p 를 만들었다. → p.Type() 을 출력해보면 *float64 라고 제대로 나온다. reflect.TypeOf(&X) 도 같은 결과였겠지? → p.CanSet() 의 결과가 중요하다. 어라! false 이다!
p 가 아니라 p.Elem() 이다.
p 자체는 set 할 수 없는 것이다. p 가 가리키는 진짜 값이 set 할 수 있는 것이다.
p.Elem() 이다. 포인터로 치면 *p 가 되겠다.
드디어 이 v 에 값을 Set 해보자
v.SetFloat(7.1) 로 설정해주고 나면 - 그 interface() 값이나 - 원래 변수인 x 가 값이 바뀐것을 알 수 있다. |
Structs
위 예제의 v 는 포인터는 아니다. 포인터에서 끌어낸 무언가이다.
이런 식의 작업이 필요한 가장 대표적인 경우는 구조체의 필드값을 변경할 때이다.
구조체의 주소값을 알고 있다면, 필드값을 변경할 수 있다.
|
예제 코드: https://play.golang.org/p/kIDgv0QJDrP
GET: 구조체 인스턴스를 reflect 로 받아서 필드명과 값을 읽어보자
1) struct 를 하나 선언하고 2) 23, "hello" 라는 초기값을 가지는 인스턴스 t 를 하나 만들었다.
3) ValueOf() 와 Elem() 을 썼으니 s 는 바로 값을 쓸 수 있는 녀석이다. settable! 4) 원래의 값인 t 의 Type 은 s.Type() 으로 알아낼 수 있다. - 타입을 안다는 것, 그리고 그 타입이 구조체라는 건 - 구조체의 필드에 접근이 가능하다는 것이겠다
for 문을 돌면서 i 번째 필드를 f 라고 하면 - 필드의 이름은 typeOfT.Field(i).Name 이 되고 - f.Type() 은 해당 필드의 타입이 되며, - f.Interface() 는 해당 필드의 값이 된다. → 즉 필드의 이름, 타입, 값을 알아낼 수 있게 된다.
SET: reflect 를 이용하여 구조체 인스턴스의 값을 바꿔보자
값을 넣어주는 건 어렵지 않다. 알아낸 필드의 타입을 기반으로 SetInt(), SetString() 등을 써주면 된다. 중요한 것은 필드명은 대문자로 시작해야 한다는 것. Export 되어야만 settable 하다.
|
Conclusion
첫 번째. 인터페이스 → 리플렉션 오브젝트로 reflection 이 가능하다 두 번째. 리플렉션 오브젝트 → 인터페이스로 값으로의 reflecton 도 가능하다 세 번째. 리플렉션 오브젝트의 값을 변경하려면 settable 해야 한다. |
'golang' 카테고리의 다른 글
go 동시성 패턴: or-done-channel 정리 (0) | 2020.07.30 |
---|---|
go build 의 -ldflags 옵션으로 빌드정보를 프로그램에 담아보자 (0) | 2020.07.22 |
Russ Cox 의 Interface 블로그 포스팅 분석 (0) | 2020.02.26 |
비밀번호 안전보관: bcrypt 를 알아보자 (2) | 2020.01.28 |
Go 깨알: if err := json.Unmarshal(bytes, &book2); err != nil { (0) | 2020.01.17 |
- Total
- Today
- Yesterday
- OpenAI
- strange
- 인텔리제이
- agile
- go
- 독서후기
- 클린 애자일
- golang
- 잡학툰
- 2023
- github
- solid
- 영화
- bun
- Bug
- intellij
- ChatGPT
- postgres
- 독서
- Gin
- 노션
- 체호프
- pool
- 제이펍
- websocket
- API
- notion
- folklore
- JIRA
- Shortcut
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |