고 고급 #4 reflect 패키지 — 런타임에 타입 다루기
#3 제네릭 다음, 이번에는 다른 결의 동적 도구. reflect.
reflect는 — 런타임에 타입을 들여다보고 조작하는 도구입니다. 직접 쓸 일은 많지 않지만 — encoding/json, text/template, ORM 같은 라이브러리 안쪽에서 항상 보입니다. 어떻게 동작하는지 알면 그 라이브러리들이 자연스러워집니다.
기본 두 개념 — Type과 Value #
import "reflect"
func main() {
x := 42
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println(t) // int
fmt.Println(v) // 42
fmt.Println(t.Kind()) // int
}reflect.Type— 타입의 메타데이터 (이름, kind, 필드, 메서드 등)reflect.Value— 값 자체와 타입 정보 둘 다
Kind — 타입의 큰 분류 #
v := reflect.ValueOf(struct{ X int }{42})
fmt.Println(v.Kind()) // structKind는 — 더 큰 분류(int, string, struct, slice, map, ptr 등). Type은 더 구체적(예: mypkg.User 라는 정의된 타입까지).
struct의 필드 들여다보기 #
type User struct {
Name string
Age int
Email string
}
u := User{"이도경", 30, "x@y.z"}
v := reflect.ValueOf(u)
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("%s %v = %v\n", field.Name, field.Type, value)
}
// Name string = 이도경
// Age int = 30
// Email string = x@y.zt.NumField(), t.Field(i), v.Field(i) — JSON 인코더가 내부에서 하는 일과 정확히 같습니다.
struct 태그 읽기 #
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
t := reflect.TypeOf(User{})
field := t.Field(0)
fmt.Println(field.Tag.Get("json")) // nameField(i).Tag.Get("json") — json:"..." 부분만 꺼냅니다. encoding/json이 이 정보로 어떻게 직렬화할지 결정합니다.
이 메커니즘이 — Go 라이브러리들이 struct만 보고 다양한 동작을 하게 만드는 핵심 원리입니다.
값 수정 — Value가 settable이어야 #
x := 42
v := reflect.ValueOf(&x).Elem() // 포인터 → 가리키는 값
v.SetInt(100)
fmt.Println(x) // 100핵심 두 가지:
- 포인터로 ValueOf 해서 —
Elem()으로 가리키는 값 얻기 - 그 값이 settable 이어야
SetInt,SetString등 가능
x := 42
v := reflect.ValueOf(x) // 값 복사
v.CanSet() // false — 복사본이라 수정 불가
v = reflect.ValueOf(&x).Elem()
v.CanSet() // true동적 함수 호출 #
func add(a, b int) int { return a + b }
f := reflect.ValueOf(add)
results := f.Call([]reflect.Value{
reflect.ValueOf(1),
reflect.ValueOf(2),
})
fmt.Println(results[0].Int()) // 3Call이 — 런타임에 함수를 부를 수 있게 해 줍니다. RPC 라이브러리, 핸들러 디스패치 같은 경우에 활용.
인터페이스 변환 — Type assertion의 동적 버전 #
var i interface{} = "hello"
v := reflect.ValueOf(i)
fmt.Println(v.Kind()) // string
fmt.Println(v.String()) // hellointerface{} 안에 무엇이 들어 있는지 — 타입 단언 없이 검사할 수 있습니다.
전형적인 사용 예 — JSON 인코딩 미니 버전 #
이해하면 reflect의 위치가 명확해집니다. 몇 줄로 만들어 봅니다.
func toMap(v any) map[string]any {
rv := reflect.ValueOf(v)
rt := rv.Type()
m := map[string]any{}
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
key := field.Tag.Get("json")
if key == "" {
key = field.Name
}
m[key] = rv.Field(i).Interface()
}
return m
}
type User struct {
Name string `json:"name"`
Age int
}
toMap(User{"이도경", 30})
// map[name:이도경 Age:30]encoding/json도 본질적으로 같은 일을 — 더 정교하게 합니다(zero value 처리, omitempty, 중첩 구조체, 슬라이스 등).
자주 만나는 경우 — 환경 변수 자동 매핑 #
type Config struct {
Port int `env:"PORT"`
Host string `env:"HOST"`
}
func loadEnv(cfg any) {
v := reflect.ValueOf(cfg).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
key := field.Tag.Get("env")
val := os.Getenv(key)
if val == "" {
continue
}
fv := v.Field(i)
switch fv.Kind() {
case reflect.String:
fv.SetString(val)
case reflect.Int:
n, _ := strconv.Atoi(val)
fv.SetInt(int64(n))
}
}
}이 패턴이 — caarlos0/env, kelseyhightower/envconfig 같은 라이브러리의 핵심 구현 방식입니다.
함정 — reflect는 느리다 #
reflect는 일반 코드보다 — 수십 배 느립니다. 이유:
- 타입 검사를 런타임에 함
- 메모리 할당이 더 많음
- 컴파일러 최적화 못 함
BenchmarkDirect-8 1000000000 0.3 ns/op
BenchmarkReflect-8 50000000 30 ns/op핫 패스에서는 — reflect 대신 코드 생성(다음 #7)을 검토하세요. 또는 interface + type switch가 reflect보다 빠른 경우도 많습니다.
함정 — 컴파일러 도움이 사라짐 #
v.SetString("hi") // 만약 v가 int 면 → 런타임 panicreflect는 정적 타입 체크를 우회합니다. 잘못 쓰면 컴파일 시점이 아니라 — 런타임 panic. 항상 Kind()로 검사하고 들어가야 안전.
함정 — interface{} 의 특수한 nil #
var p *User = nil
var i interface{} = p
i == nil // false — interface는 (타입, 값) 쌍
v := reflect.ValueOf(i)
v.IsNil() // true — Value의 IsNil은 안쪽 값을 봄interface가 nil이려면 타입과 값이 모두 nil 이어야 합니다. reflect.Value.IsNil()이 — 안쪽 포인터의 nil 여부를 알려 줍니다(Kind에 따라).
unsafe.Pointer와의 관계
#
reflect는 — 내부적으로 unsafe.Pointer를 활용합니다. 그래서 안전하지만 — 잘못 쓰면 같은 위험을 만들 수 있습니다. 보통 라이브러리 작성자만 깊게 들어가고, 일반 사용자는 표면 API만 씁니다.
#5 unsafe와 cgo에서 unsafe를 다룹니다.
언제 써야 하나? — 회피 가능하면 회피 #
reflect의 원칙:
- 컴파일 타임 정보로 충분하면 → 제네릭, 인터페이스, 코드 생성
- 타입을 모르고도 동작해야 하는 라이브러리 → reflect가 적절
대부분의 비즈니스 로직은 — 첫 번째에 해당. reflect를 직접 쓸 일은 라이브러리/프레임워크 코드에서 더 흔합니다.
“Reflection is never clear.” — Go proverbs
실전 — fmt.Println도 reflect의 사용자
#
fmt.Printf("%v", x)가 — 임의의 타입을 받아 출력 가능한 이유는 reflect. 표준 라이브러리에서 대표적인 사용 사례입니다.
fmt.Sprintf("%v", User{"이도경", 30}) // {이도경 30}마무리 #
이번 글에서 정리한 내용:
reflect.Type— 타입 메타데이터,reflect.Value— 값 + 타입Kind— 타입의 큰 분류 (int, struct, ptr 등)- struct 필드/태그 순회 —
NumField,Field,Tag.Get - 값 수정 — 포인터 →
Elem()→ settable 인지 확인 →Set* - 동적 호출 —
reflect.Value.Call - 느림 — 핫 패스 회피, 핫하면 코드 생성으로 대체
- 정적 타입 체크 우회 — 런타임 panic 위험
- 가능하면 제네릭/인터페이스 우선
다음 글(#5 unsafe와 cgo)에서는 — Go의 안전 영역 밖으로 한 걸음 나가는 두 도구를 정리합니다. 거의 안 쓰지만, 어떤 경우가 그 도구를 부르는지는 알아 둘 가치가 있습니다.