728x90

Go 언어에서 메서드를 정의할 때, 구조체와 메서드의 관계를 결정하는 중요한 요소 중 하나는 리시버(Receiver)의 타입임

리시버는 메서드가 호출될 때 어떤 방식으로 구조체 데이터를 다룰지 결정하며, 값 리시버포인터 리시버 두 가지 방식이 있음

값 리시버란?

값 리시버는 메서드 호출 시, 구조체의 복사본을 리시버로 받는 방식

즉, 메서드가 호출될 때 원본 구조체가 아닌, 그 복사된 값이 메서드로 전달됨

package main

import "fmt"

type Counter struct {
    count int
}

func (c Counter) Increment() {
    c.count++
}

func (c Counter) GetCount() int {
    return c.count
}

func main() {
    counter := Counter{}
    counter.Increment()
    fmt.Println(counter.GetCount())  // 출력: 0
}

위 코드에서 Increment() 메서드는 값 리시버를 사용함

메서드가 호출될 때 Counter 구조체의 복사본이 전달되기 때문에, Increment()가 count 값을 증가시키더라도 원본 구조체의 값은 변하지 않음

따라서 GetCount() 메서드로 값을 확인하면 여전히 0이 출력됨

Increment 메서드는 아무런 의미가 없는 메서드라고 볼 수 있음

포인터 리시버란?

포인터 리시버는 메서드 호출 시, 구조체의 메모리 주소를 전달받아 원본 데이터를 직접 수정할 수 있는 방식

포인터 리시버를 사용하면 메서드 내부에서 구조체의 상태를 변경할 수 있으며, 메모리 주소를 다루기 때문에 큰 구조체를 복사하지 않고도 성능을 최적화할 수 있음

package main

import "fmt"

type Counter struct {
    count int
}

func (c *Counter) Increment() {
    c.count++
}

func (c *Counter) GetCount() int {
    return c.count
}

func main() {
    counter := Counter{}
    counter.Increment()
    fmt.Println(counter.GetCount())  // 출력: 1
}

위 코드에서 Increment() 메서드는 포인터 리시버를 사용함

이 메서드는 구조체의 포인터를 전달받아, 원본 구조체의 count 값을 직접 수정할 수 있음

따라서 Increment() 메서드를 호출한 후 GetCount() 메서드를 통해 수정된 값을 확인할 수 있으며, 출력은 1이 됨

값 리시버와 포인터 리시버의 차이점

구분 값 리시버 포인터 리시버
구조체 전달 방식 구조체의 복사본 전달 구조체의 메모리 주소 전달
원본 수정 여부 원본 수정 불가능 원본 수정 가능
메모리 효율성 구조체가 클 경우 비효율적 (복사 비용) 메모리 효율적
상태 변경 메서드 내에서 상태를 변경할 수 없음 메서드 내에서 상태를 변경할 수 있음

성능 차이

값 리시버는 구조체의 복사본을 사용하기 때문에, 큰 구조체에서는 메모리 복사로 인해 성능 저하가 발생할 수 있음

반면, 포인터 리시버는 구조체의 메모리 주소를 사용하므로 성능적으로 더 효율적

상태 변경

값 리시버는 구조체의 상태를 변경하지 않음

즉, 복사본에서만 변경이 일어나므로 원래의 구조체 상태에는 영향을 미치지 않음

반면, 포인터 리시버는 원본 구조체의 데이터를 직접 수정할 수 있어 상태 변경이 필요한 경우 적합함

언제 값 리시버를 사용하고, 언제 포인터 리시버를 사용해야 할까

  • 값 리시버가 유리한 경우
    • 구조체가 작고, 복사 비용이 적으며, 동시성 문제를 피해야 하는 경우 값 리시버가 유리함
    • 값 리시버는 복사본을 사용하기 때문에 원본 데이터의 상태를 변경할 수 없으며, 여러 고루틴이 안전하게 동시 접근할 수 있음
  • 포인터 리시버가 유리한 경우
    • 구조체가 크거나, 원본 상태를 변경할 필요가 있는 경우 포인터 리시버가 유리함
    • 그러나 동시성 문제를 신경 써야 하며, 성능 이점을 얻으려면 구조체 크기와 복사 비용을 고려해야 함

값 리시버와 포인터 리시버를 모두 사용하는 예제

하나의 구조체에 값 리시버와 포인터 리시버를 모두 사용할 수 있음

package main

import "fmt"

type Person struct {
    name string
    age  int
}

// 값 리시버: 상태를 변경하지 않음
func (p Person) GetName() string {
    return p.name
}

// 포인터 리시버: 상태를 변경함
func (p *Person) HaveBirthday() {
    p.age++
}

func main() {
    person := Person{name: "Alice", age: 25}

    fmt.Println("이름:", person.GetName()) // 값 리시버 사용
    person.HaveBirthday()                 // 포인터 리시버 사용
    fmt.Println("나이:", person.age)       // 출력: 26
}

GetName() 메서드는 값 리시버를 사용하여 데이터를 읽기만 하며, HaveBirthday() 메서드는 포인터 리시버를 사용하여 age 필드를 직접 수정함

 

 

728x90

'Go' 카테고리의 다른 글

Go 함수와 메서드 차이  (0) 2024.06.24
Go 구조체, 메서드, 인터페이스  (0) 2024.06.19

+ Recent posts