Go에서 값 리시버 vs 포인터 리시버 (실용적인 설명)
Source: Dev.to
Introduction
Go에서 처음 마주치는 설계 질문 중 하나는 메서드가 값 리시버를 사용할지 포인터 리시버를 사용할지 결정하는 것입니다. 두 형태는 모두 자주 보입니다:
func (u User) Greet() {}
func (u *User) UpdateName() {}
비슷해 보이지만, 선택은 정확성, 성능, 그리고 실제 백엔드 코드에서 타입이 동작하는 방식에 영향을 미칩니다.
Value Receivers
Go에서 메서드는 타입에 붙어 있는 함수에 불과합니다.
type User struct {
Name string
}
값 리시버로 동작을 추가하기:
func (u User) Greet() {
fmt.Println("Hello,", u.Name)
}
여기서 u는 리시버라고 부르며, 메서드가 호출된 값의 복사본입니다.
user := User{Name: "Shivam"}
user.Greet()
Modifying a Value Receiver
메서드가 복사본을 받으면, 모든 수정은 그 복사본에만 영향을 미칩니다.
func (u User) ChangeName() {
u.Name = "New Name"
}
예시
package main
import "fmt"
type User struct {
Name string
}
func (u User) ChangeName() {
u.Name = "New Name"
}
func main() {
user := User{Name: "Shivam"}
user.ChangeName()
fmt.Println(user.Name) // Output: Shivam
}
출력은 Shivam 그대로입니다. 메서드가 복사본에서 동작했기 때문입니다.
핵심 포인트: 값 리시버 = 복사본에서 작업한다.
When to Use Value Receivers
- 구조체가 작을 때.
- 메서드가 리시버를 수정할 필요가 없을 때(읽기 전용 헬퍼).
전형적인 예는 Point 타입입니다:
func (p Point) Distance() float64 { /* ... */ }
Pointer Receivers
포인터 리시버는 메서드가 메모리상의 원본 구조체에 접근하도록 합니다.
func (u *User) ChangeName() {
u.Name = "New Name"
}
예시
package main
import "fmt"
type User struct {
Name string
}
func (u *User) ChangeName() {
u.Name = "New Name"
}
func main() {
user := User{Name: "Shivam"}
user.ChangeName()
fmt.Println(user.Name) // Output: New Name
}
변경이 지속되는 이유는 메서드가 원본 객체에서 동작했기 때문입니다.
핵심 포인트: 포인터 리시버 = 원본에서 작업한다.
Advantages of Pointer Receivers
- 상태 수정: 필드를 업데이트해야 하는 메서드는 거의 항상 포인터 리시버를 사용해야 합니다.
- 성능: 큰 구조체는 매 메서드 호출 시 복사 비용을 피할 수 있습니다.
- 인터페이스 구현: 포인터 리시버를 가진 메서드는 포인터 타입에만 속하므로, 타입이 인터페이스를 만족하는지에 영향을 줍니다.
Choosing Between Value and Pointer Receivers
| Situation | Recommended Receiver |
|---|---|
| 메서드가 리시버의 필드를 수정해야 함 | 포인터 리시버 (*T) |
| 구조체가 크고 복사가 비용이 많이 듦 | 포인터 리시버 |
| 메서드가 데이터를 읽기만 하고 구조체가 작음 | 값 리시버 (T) |
| 포인터 메서드가 필요한 인터페이스를 구현해야 함 | 포인터 리시버 |
실제로 백엔드 시스템에서 실제 엔티티(서비스, 핸들러, 설정, DB 모델, 컨트롤러)를 나타내는 타입이라면 포인터 리시버가 기본 선택입니다. 불변 데이터처럼 동작하는 간단한 값 타입이라면 값 리시버도 괜찮습니다.
Summary
- 값 리시버: 복사본에서 동작; 수정이 메서드 밖에 반영되지 않음.
- 포인터 리시버: 원본에서 동작; 수정이 지속됨.
- 상태를 수정하거나 큰 구조체 복사를 피하거나 포인터 메서드가 필요한 인터페이스를 만족해야 할 때는 포인터 리시버를 사용하세요.
- 작고 읽기 전용인 타입에는 값 리시버를 사용하세요.
이 구분을 몸에 익히면 Go에서 메서드 설계가 훨씬 쉬워집니다.