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 언어의 reflect 패키지: 첫걸음부터 실전 활용까지 (1) | 2024.11.08 |
---|---|
Go 언어의 `:=` 연산자, 그 숨겨진 이야기! (1) | 2024.10.30 |
Goroutine에서 `os.Chdir()` 사용할 때 발생하는 문제와 해결 방법 (0) | 2024.10.06 |
Go 1.18에서 `any`로 더 간결하게: `interface{}`의 진화 (0) | 2024.10.06 |
Go 언어 `reflect` 패키지 완벽 가이드: 런타임 타입 처리를 마스터하자 (0) | 2024.10.06 |