Go언어

go reflect 이해하기

stdhsw 2026. 3. 22. 05:36

reflect란?

Go 언어는 강력한 정적 타입(Statically Typed) 언어입니다. 하지만 실무를 하다 보면 ORM 라이브러리를 만들거나, Kubernetes 매니페스트 같은 복잡한 YAML/JSON 데이터를 파싱할 때처럼 런타임에 변수의 타입을 검사하고 제어해야 하는 순간이 반드시 찾아옵니다.

이럴때 사용할 수 있는게 바로 reflect 패키지입니다. 이번 문서에서는 Golang의 reflect 패키지를 활용해 프레임워크나 공통 라이브러리, 직렬화/역직렬화(예: JSON) 도구 등을 구현할 수 있는 방법에 대하여 알아보겠습니다.

 

reflect 사용하기

Reflection의 기초: Type과 Kind

Reflection의 시작점은 `interface{}` (any) 입니다. 빈 인터페이스 내부에는 데이터의 '타입(Type)'과 '값(Value)'을 가리키는 포인터가 숨어 있으며, `reflect` 패키지는 이 정보를 추출합니다.

가장 먼저 짚고 넘어가야 할 것은 Type과 Kind의 차이입니다.

  • Type: 프로그래머가 부여한 '이름' (예: `type MyInt int`에서의 `MyInt`)
  • Kind: Go 언어가 인식하는 본질적인 '자료구조' (예: `int`, `struct`, `ptr`)

reflect.TypeOf()와 reflect.ValueOf() 함수는 바로 이 인터페이스 내부의 'Type' 포인터와 'Value' 포인터 정보를 추출해 내는 역할을 합니다.

// 사용자 정의 타입
type ID int
type User struct {
	Name string
}

func main() {
	var id ID = 123
	var u User = User{Name: "Gopher"}

	tID := reflect.TypeOf(id)
	tUser := reflect.TypeOf(u)

	// 1. Type과 Kind 비교
	fmt.Printf("[ID 변수] Type: %s, Kind: %s\n", tID.Name(), tID.Kind()) 
	// 출력: [ID 변수] Type: ID, Kind: int

	fmt.Printf("[User 변수] Type: %s, Kind: %s\n", tUser.Name(), tUser.Kind()) 
	// 출력: [User 변수] Type: User, Kind: struct

	// 2. 포인터 타입의 주의점
	ptrUser := &u
	tPtr := reflect.TypeOf(ptrUser)

	fmt.Printf("\n[포인터 변수] Type: '%s', Kind: %s\n", tPtr.Name(), tPtr.Kind())
	// 출력: [포인터 변수] Type: '', Kind: ptr
	// 주의: 포인터 자체는 이름(Name)이 없습니다. Kind는 ptr(포인터)로 나옵니다.

	// 포인터가 가리키는 원본의 타입을 알고 싶다면 Elem()을 사용해야 합니다.
	fmt.Printf("[포인터 원본] Type: %s, Kind: %s\n", tPtr.Elem().Name(), tPtr.Elem().Kind())
	// 출력: [포인터 원본] Type: User, Kind: struct
}

 

 

Value의 이해와 값의 동적 조작

타입 정보를 확인했다면, 이제 reflect.ValueOf()를 통해 실제 값을 읽고 쓸 차례입니다. 런타임에 변수의 값을 동적으로 변경하려면 두 가지 철칙을 지켜야 합니다.

  • 포인터를 넘길 것: 값 복사가 일어나면 원본을 수정할 수 없습니다.
  • Elem()으로 접근할 것: 포인터 자체가 아닌 메모리가 가리키는 실제 공간으로 이동해야 합니다.
func main() {
	score := 100

	// ❌ [실패 사례] 값을 그대로 복사해서 넘겼을 때
	vFail := reflect.ValueOf(score)
	fmt.Printf("vFail은 변경 가능한가? %v\n", vFail.CanSet()) 
	// 출력: false
	// vFail.SetInt(200) // 런타임 Panic 발생! (원본 주소를 모르기 때문)

	// ✅ [성공 사례] 포인터(&)를 넘기고 Elem()으로 원본에 접근했을 때
	vSuccess := reflect.ValueOf(&score).Elem()
	fmt.Printf("vSuccess는 변경 가능한가? %v\n", vSuccess.CanSet()) 
	// 출력: true

	if vSuccess.CanSet() {
		vSuccess.SetInt(200) // 원본 값 200으로 변경
	}

	fmt.Printf("변경된 score 원본 값: %d\n", score) 
	// 출력: 200
}

 

구조체(Struct)와 태그(Tag) 파싱 실전

실무에서 Reflection이 가장 빛나는 곳입니다. t.NumField()와 v.Field(i)를 활용해 구조체의 모든 필드를 순회하며 데이터를 동적으로 제어할 수 있습니다. 특히 백틱(`)으로 감싼 구조체 태그(Struct Tag)를 파싱하는 기술은 필수입니다.

type PodSpec struct {
    Image string `yaml:"image" validate:"required"`
}

func main() {
    t := reflect.TypeOf(PodSpec{})
    field := t.Field(0)

    yamlKey := field.Tag.Get("yaml")          // "image"
    rule, ok := field.Tag.Lookup("validate")  // "required", true
}

 

함수와 메서드의 동적 호출

이름만 알고 있는 함수나 메서드를 런타임에 실행하려면 .Call()을 사용합니다. 이때 주고받는 모든 매개변수와 반환값은 []reflect.Value (Value 슬라이스) 형태로 포장해야 합니다. 구조체 인스턴스에 묶인 메서드를 호출할 때는 .MethodByName("메서드명")을 사용합니다.

type Calculator struct{}

func (c *Calculator) Multiply(a, b int) int {
	return a * b
}

func main() {
    calc := &Calculator{}
    method := reflect.ValueOf(calc).MethodByName("Multiply")

    args := []reflect.Value{reflect.ValueOf(5), reflect.ValueOf(6)}
    result := method.Call(args)
    fmt.Println(result[0].Int()) // 30
}

 

복합 타입(Slice, Map)의 동적 생성

컴파일 타임에 타입의 구조를 전혀 알 수 없을 때, 아예 런타임에 새로운 컬렉션을 메모리에 동적 할당할 수도 있습니다. Python의 딕셔너리처럼 유연한 데이터 컨테이너를 Go로 구현할 때 유용합니다.

  • Slice: reflect.MakeSlice로 생성 후 reflect.Append로 요소 추가
  • Map: reflect.MakeMap으로 생성 후 SetMapIndex(키, 값)으로 제어
func main() {
    mapType := reflect.TypeOf(map[string]int{})
    vMap := reflect.MakeMap(mapType)

    key := reflect.ValueOf("CPU_Cores")
    val := reflect.ValueOf(4)
    vMap.SetMapIndex(key, val)
}

 

마무리하며

Golang의 reflect는 복잡한 직렬화 로직이나 동적 라우팅 시스템을 우아하게 풀어낼 수 있는 마법 같은 도구입니다. 하지만 Reflection은 컴파일러의 최적화를 우회하므로 잦은 사용은 성능 병목을 일으킬 수 있다는 점을 항상 명심해야 합니다.