
Go 언어의 리플렉션, 제대로 활용하는 방법!
Go 언어의 리플렉션(Reflection) 기능, 왠지 어렵고 복잡하게 느껴지시나요?
하지만 리플렉션은 Go 언어의 강력한 기능 중 하나이며, 제대로 이해하고 활용하면 코드의 유연성과 확장성을 크게 높일 수 있습니다.
이 글에서는 Go 언어의 리플렉션 기능을 쉽고 명확하게 설명하고, 실제 코드 예시를 통해 제대로 활용하는 방법을 알려드리겠습니다.
Go 언어의 리플렉션, 핵심 개념부터 짚고 넘어가자!
리플렉션이란?
모든 변수를
interface{}타입으로 다루면서, 변수의 실제 값을 기반으로 유연한(동적 타입 언어처럼) 연산을 수행하는 기능입니다.
Go 언어의 타입
Go 언어는 정적 타입 언어입니다. 즉, 변수의 타입이 컴파일 시점에 결정됩니다.
type MyInt int
var i int // i에는 MyInt 타입 값을 타입 변환 없이 대입할 수 없습니다.
var j MyInt // j에는 int 타입 값을 타입 변환 없이 대입할 수 없습니다.
i와 j의 기저 타입(Underlying Type)은 둘 다 int이지만, 타입 변환 없이는 값을 대입할 수 없습니다.
Go 언어의 인터페이스
type Greeter interface {
Hello(name string)
}
type Person struct {
name string
age int
}
func (p *Person) Hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
Person 구조체는 Hello 메서드를 구현했으므로, Greeter 인터페이스를 충족합니다.
특정 메서드를 구현하면, 어떤 값이든 인터페이스 타입으로 다룰 수 있습니다.
interface{} (또는 any)는 어떤 메서드도 지정하지 않았으므로, 모든 타입이 이 인터페이스를 충족합니다.
따라서, interface{} 타입 변수의 실제 값은 런타임에 어떤 타입으로든 변경될 수 있습니다.
모든 타입은 interface{} 타입으로 처리될 수 있습니다.
Go 언어의 인터페이스
인터페이스 타입 변수는 변수의 실제 값과 그 값의 타입 식별자를 갖습니다.
type Greeter interface {
Hello(name string)
}
func (p *Person) Hello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
type Runner interface {
Run()
}
func (p *Person) Run() {
fmt.Println("running...")
}
var g Greeter
p := Person{name: "John", age: 20}
p.Hello("James") // Hello, James!
g = p
g는 Greeter 타입이지만, 내부적으로 다음 두 가지 정보를 가지고 있습니다.
- 실제 값:
{name: "John", age: 20} - 값의 타입 식별자:
Person
이 정보 덕분에 다음과 같은 타입 변환이 가능합니다.
var r Runner
r = g.(Runner)
Greeter 타입을 Runner 타입으로 변환할 수 있는 이유는 다음 조건이 충족되기 때문입니다.
g(Greeter타입)가 실제 값의 타입(Person)을 알고 있습니다.Person은Runner인터페이스를 충족합니다.
r은 Runner 타입이지만, g와 동일한 정보를 내부적으로 가지고 있습니다.
- 실제 값:
{name: "John", age: 20} - 값의 타입 식별자:
Person
마찬가지로, 다음과 같은 변환도 가능합니다.
var empty interface{}
empty = r
empty는 interface{} 타입이지만, g, r과 동일한 정보를 내부적으로 가지고 있습니다.
Go 언어의 리플렉션, 다시 한번 정리!
interface{} 타입 변수에 저장된 실제 값과 타입을 기반으로 동작을 제어하는 기능입니다.
Go 언어의 리플렉션은 다음 세 가지 규칙을 따릅니다.
- 인터페이스 값을 리플렉션 객체로 변환할 수 있습니다.
- 리플렉션 객체를 인터페이스 값으로 변환할 수 있습니다.
- 리플렉션 객체를 수정하려면 값을 설정할 수 있어야 합니다.
1. 인터페이스 값에서 리플렉션 객체로 변환
인터페이스 타입 변수에 저장된 값과 타입을 검사하는 기능입니다.
저장된 값은 reflect 패키지의 Value, 타입은 Type으로 확인할 수 있습니다.
func main() {
err := fmt.Errorf("error")
fmt.Println("type:", reflect.TypeOf(err)) // type: *errors.errorString
}
reflect.TypeOf 함수의 시그니처(Signature)는 다음과 같습니다.
func TypeOf(i interface{}) Type
따라서, 위 코드의 err 변수는 interface{} 타입으로 처리됩니다.
reflect.ValueOf() 함수를 사용하여 interface{} 변수에 저장된 실제 값을 확인할 수 있고, Kind() 메서드를 사용하여 interface{} 변수에 저장된 값이 reflect 패키지에서 어떻게 표현되는지 확인할 수 있습니다.
func main() {
err := fmt.Errorf("error")
value := reflect.ValueOf(err)
fmt.Println("type:", value.Type()) // type: *errors.errorString
fmt.Println("kind is pointer:", value.Kind() == reflect.Ptr) // kind is pointer: true
fmt.Println("type:", value.String()) // type: <*errors.errorString Value>
}
Kind() 메서드는 저장된 값의 기저 타입을 반환합니다.
func main() {
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
fmt.Println(v.Kind()) // int (MyInt가 아닙니다!)
}
2. 리플렉션 객체에서 인터페이스 값으로 변환
ValueOf(i any) Value의 반환 값을 interface{} 타입으로 변환하는 메서드가 있습니다.
func (v Value) Interface() interface{}
다음과 같이 x에서 가져온 Value (리플렉션 객체)를 interface{} 타입으로 변환하고, 이를 float64 타입으로 변환하여 사용할 수 있습니다.
func main() {
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
y := v.Interface().(float64)
fmt.Println(y)
}
(참고) 실제로는 interface{}에서 float64로의 타입 변환은 Println 함수 내부에서 자동으로 처리되므로, 위와 같이 명시적으로 변환할 필요는 없습니다.
3. 리플렉션 객체 수정, Settable 속성이 핵심!
Settable 속성은 포인터를 통해 변수를 수정할 수 있는지 여부를 나타냅니다.
쉽게 말해, 리플렉션 객체가 원본 변수를 직접 수정할 수 있는 능력입니다.
리플렉션 객체가 원본 값을 가지고 있는지에 따라 결정됩니다.
Addressability
예를 들어, person 변수의 값을 함수에 전달하는 것과 person 변수의 포인터를 전달하는 것은 person 변수의 속성을 변경할 수 있는지에 영향을 미칩니다.
type Person struct {
Age int
}
func stayYoung(p Person) {
p.Age += 1
}
func getOld(p *Person) {
p.Age += 1
}
func main() {
person := Person{
Age: 19,
}
stayYoung(person)
fmt.Println(person.Age) // 19
getOld(&person)
fmt.Println(person.Age) // 20
}
Settability
func main() {
var original float64 = 3.4
copied := reflect.ValueOf(original) // 값 복사, Settable 속성 없음
fmt.Println(copied.CanSet()) // false
pointer := reflect.ValueOf(&original) // 포인터 자체는 Settable 속성 없음
fmt.Println(pointer.CanSet()) // false
reflectionValue := pointer.Elem() // 포인터에서 원본 값을 가져오면 Settable 속성 획득
fmt.Println(reflectionValue.CanSet()) // true
}
reflectionValue는 Settable 속성을 가지므로 값을 수정할 수 있습니다.
reflectionValue.SetFloat(7.1)
fmt.Println(reflectionValue) //7.1
}
마무리
Go 언어의 리플렉션은 강력하지만, 신중하게 사용해야 합니다.
리플렉션을 과도하게 사용하면 코드의 가독성과 유지 보수성이 떨어질 수 있습니다.
하지만 적재적소에 활용하면 코드의 유연성과 확장성을 크게 향상시킬 수 있습니다.
이 글에서 설명한 내용을 바탕으로, Go 언어의 리플렉션 기능을 효과적으로 활용하여 더 나은 코드를 작성해 보세요!
'Go' 카테고리의 다른 글
| Go에서 구조체 패킹(structure packing) 이해하기 (0) | 2024.12.28 |
|---|---|
| Go 언어의 reflect 패키지: 첫걸음부터 실전 활용까지 (1) | 2024.11.08 |
| Go 언어의 `:=` 연산자, 그 숨겨진 이야기! (1) | 2024.10.30 |
| Goroutine에서 `os.Chdir()` 사용할 때 발생하는 문제와 해결 방법 (0) | 2024.10.06 |
| Go 1.18에서 `any`로 더 간결하게: `interface{}`의 진화 (1) | 2024.10.06 |