Go

Go 언어의 리플렉션, 제대로 활용하는 방법!

드리프트2 2024. 10. 30. 21:11

Go 언어의 리플렉션, 제대로 활용하는 방법!

 

Go 언어의 리플렉션(Reflection) 기능, 왠지 어렵고 복잡하게 느껴지시나요?

 

하지만 리플렉션은 Go 언어의 강력한 기능 중 하나이며, 제대로 이해하고 활용하면 코드의 유연성과 확장성을 크게 높일 수 있습니다.

 

이 글에서는 Go 언어의 리플렉션 기능을 쉽고 명확하게 설명하고, 실제 코드 예시를 통해 제대로 활용하는 방법을 알려드리겠습니다.

 

 

Go 언어의 리플렉션, 핵심 개념부터 짚고 넘어가자!

 

리플렉션이란?

모든 변수를 interface{} 타입으로 다루면서, 변수의 실제 값을 기반으로 유연한(동적 타입 언어처럼) 연산을 수행하는 기능입니다.

 

 

Go 언어의 타입

 

Go 언어는 정적 타입 언어입니다. 즉, 변수의 타입이 컴파일 시점에 결정됩니다.

type MyInt int 

var i int    // i에는 MyInt 타입 값을 타입 변환 없이 대입할 수 없습니다.
var j MyInt  // j에는 int 타입 값을 타입 변환 없이 대입할 수 없습니다.

 

ij의 기저 타입(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

 

gGreeter 타입이지만, 내부적으로 다음 두 가지 정보를 가지고 있습니다.

  • 실제 값: {name: "John", age: 20}
  • 값의 타입 식별자: Person

이 정보 덕분에 다음과 같은 타입 변환이 가능합니다.

var r Runner
r = g.(Runner)

 

Greeter 타입을 Runner 타입으로 변환할 수 있는 이유는 다음 조건이 충족되기 때문입니다.

  • g(Greeter 타입)가 실제 값의 타입(Person)을 알고 있습니다.
  • PersonRunner 인터페이스를 충족합니다.

rRunner 타입이지만, g와 동일한 정보를 내부적으로 가지고 있습니다.

  • 실제 값: {name: "John", age: 20}
  • 값의 타입 식별자: Person

마찬가지로, 다음과 같은 변환도 가능합니다.

var empty interface{}
empty = r

 

emptyinterface{} 타입이지만, g, r과 동일한 정보를 내부적으로 가지고 있습니다.

 

Go 언어의 리플렉션, 다시 한번 정리!

interface{} 타입 변수에 저장된 실제 값과 타입을 기반으로 동작을 제어하는 기능입니다.

 

Go 언어의 리플렉션은 다음 세 가지 규칙을 따릅니다.

  1. 인터페이스 값을 리플렉션 객체로 변환할 수 있습니다.
  2. 리플렉션 객체를 인터페이스 값으로 변환할 수 있습니다.
  3. 리플렉션 객체를 수정하려면 값을 설정할 수 있어야 합니다.

 

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 언어의 리플렉션 기능을 효과적으로 활용하여 더 나은 코드를 작성해 보세요!