Post

17. 메서드

17. 메서드

✅ 1. 메서드 선언

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type account struct {
	balance int
}

func (a *account) withdraw(amount int) {
	a.balance -= amount
}

func main() {
	a := &account{100}

	a.withdraw(20)
	fmt.Printf("%d \n", a.balance)
}
  • (a *account)와 같이 func 키워드와 함수명 사이에 리시버를 정의해서 메서드를 선언한다
  • a와 같은 구조체 변수는 메서드 내에서 매개변수처럼 사용된다
  • 리시버로는 해당 패키지 안에서 type 키워드로 선언된 로컬 타입만 가능하다
  • 메서드는 구조체의 필드에 접근하는 것처럼 점 연산자로 호출한다

메서드 정의는 패키지 내의 어디에도 위치할 수 있다. 하지만 리시버 타입이 선언된 파일 내에서 정의하는 것이 일반적인 규칙이다.

1.1 별칭 리시버 타입

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

type myInt int

func (mi myInt) add(b int) int {
	return int(mi) + b
}

func main() {
	var a myInt = 10
	fmt.Println(a.add(5)) // 15
	var b int = 10
	fmt.Println(myInt(b).add(5)) // 15
}

  • 모든 로컬 타입이 리시버 타입으로 가능하기 때문에 별칭 타입도 리시버가 될 수 있고 메서드를 가질 수 있다
  • int와 같은 내장 타입도 별칭 타입을 활용하면 메서드를 가질 수 있다

✅ 2. 메서드는 왜 필요한가?

  • 좋은 프로그래밍을 위해서는 결합도를 낮추고 응집도를 높여야 한다. 메서드는 데이터와 관련된 기능을 묶기 때문에 코드 응집도를 높이는 데 중요한 역할을 한다
  • 현대 프로그래밍에서는 함수 호출 순서(절자치향)보다는 객체를 만들고 다른 객체와의 상호 작용을 맺는 것(객체지향)이 더 중요해졌고, 객체 간의 상호작용이 메서드로 표현된다

✅ 3. 포인터 메서드 vs 값 타입 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

type account struct {
	balance int
	name    string
}

func (a1 *account) withdraw_pointer(amount int) {
	a1.balance -= amount
}

func (a2 account) withdraw_value(amount int) {
	a2.balance -= amount // ineffective assignment to field account.balance
}

func (a3 account) withdraw_returnValue(amount int) account {
	a3.balance -= amount
	return a3
}

func main() {
	var acc *account = &account{100, "Joe"}

	acc.withdraw_pointer(30) // 1. 포인터 메서드 호출
	fmt.Println(acc.balance) // 70

	acc.withdraw_value(20)   // 2. 값 타입 메서드 호출
	fmt.Println(acc.balance) // 70

	var acc2 account = acc.withdraw_returnValue(20) // 3. 값 타입 리턴 메서드 호출
	fmt.Println(acc2.balance)                       // 50
}
  • 포인터 메서드를 호출하면 포인터가 가리키는 메모리 주솟값이 복사되어 함수 내부, 외부가 동일한 인스턴스를 가리킨다
  • 값 타입 메서드를 호출하면 인스턴스 복사가 일어나 함수 내부, 외부의 인스턴스는 서로 다른 메모리 주소를 가지게 된다
  • 값 타입 메서드의 결과값을 반환하여 새로운 인스턴스에 대입하면 인스턴스 복사가 두 번 발생한다
    • a3, acc2, acc 모두 다른 주소를 가지는 인스턴스

💡위 예제에서 *account 타입의 acc 인스턴스로 account 타입을 리시버로 가지는 withdraw_value 메서드를 호출했다.
Go 언어에서는 *Type 타입 인스턴스는 Type 리시버 메서드와 *Type 리시버 메서드를 모두 호출할 수 있다.
또한 Type 타입 인스턴스는 원칙적으로 Type 리시버 메서드만 가지고 있지만 해당 타입이 addressable하다면 컴파일러가 &를 붙여서 *Type 타입 리시버 메서드도 호출할 수 있도록 해준다.

✅ 4. B.4 값 타입을 쓸 것인가? 포인터를 쓸 것인가?

4.1 B.4.1 성능에는 거의 차이가 없다

  • 포인터가 물론 더 적은 메모리 복사가 이루어지긴 하지만 Go에서는 메모리를 많이 차지하는 슬라이스, 문자열, 맵 등이 모두 내부 포인털르 가지는 형태로 제작되어 있어 값 복사에 따른 메모리 낭비를 걱정하지 않아도 된다. (일반적인 경우엔)

4.2 B.4.2 객체 성격에 맞춰라

1
2
3
4
5
// time.Time 객체가 가지는 메서드 목록
type Time
	func Now() Time
	func (t Time) Add(d Duration) Time
	func (t Time) After(u Time) bool
  • Time 객체는 값 타입으로 사용되는 객체다
  • 시간이라는 객체의 성격을 볼 때, 값(시간)이 변화하면 이전 시간과는 다른 객체가 되어야 하는 것이 타당하다
1
2
3
4
5
type Timer
	func AfterFunc(d Duration, f func()) *Timer
	func NewTimer(d Duration) *Timer
	func (t *Timer) Reset(d Duration) bool
	func (t *Timer) Stop() bool 
  • Timer 객체는 리시버 타입에 포인터를 사용한다
  • 30초 이후에 알림을 주는 Timer 객체를 생성한 후 타이머가 종료되었다고 해서 이 Timer 객체가 다른 객체로 변하는 것은 타당하지 않다

  • 두 방식에 문법적인 차이는 없지만 성격이 다르므로 개발자가 상황에 맞게 선택해야 한다
This post is licensed under CC BY 4.0 by the author.