Go를 배워보자 10일차 - 구조체2, 메서드와 리시버 매개변수

0

Go

목록 보기
10/12

구조체 2

지난 번에는 구조체를 선언하고, 이를 사용하는 방법을 배웠다. 그러나, 구조체는 이것이 다가 아니다. c언어에서는 구조체에 함수를 넣어줄 수 없지만, c++이 되면서 구조체에 함수를 넣어줄 수 있었다.

이것이 필요한 이유는 사용자 정의 타입에서 메서드를 정의하면 메서드 이름이 같지만 다른 사용자 정의 타입 간에는 충돌이 발생하지 않기 때문이다.

golang 역시도 마찬가지 이다. 다음의 문제 상황을 해결해보자

package main

import "fmt"

type Coffee struct {
	name  string
	price int
}

type Tea struct {
	name  string
	price int
}

func drinkCoffee(coffee Coffee, money *int) {
	fmt.Println("i drink " + coffee.name)
	*money = *money - coffee.price
}

func drinkTea(tea Tea, money *int) {
	fmt.Println("i drink " + tea.name)
	*money = *money - tea.price
}

func main() {
	totalMoney := 100
	coffee := Coffee{
		name:  "india",
		price: 10,
	}
	tea := Tea{
		name:  "korea",
		price: 20,
	}
	drinkCoffee(coffee, &totalMoney)            // i drink india
	drinkTea(tea, &totalMoney)                  // i drink korea
	fmt.Println("remain money is ", totalMoney) // remain money is  70
}

뭔가 코드가 긴 것 같지만, 간단하다. Coffee와 Tea 타입이 있는데, 이를 마시는 메서드가 각각이 존재한다. 그러나 내부 동작을 고려해보면 사실 다를 바 없는 함수이다. 차라리 각 타입마다 같은 기능을 하는 함수을 넣되, 함수 이름은 drink() 로 통일하는게 어떻까??

실제로 C는 구조체에 함수를 넣을 수 없어서 이렇게 프로그래밍을 해야한다. c++, java, python, js같은 경우에는 클래스에 메서드를 넣을 수 있어서 해결할 수 있다.

golang은 어떻게해야할까??
재밌게도 golang은 이들의 중간점을 택했다. 아주 재밌는데, c언어처럼 struct키워드 안에 메서드를 정의할 수는 없다.

그러나 그렇다고 위에 처럼 따로 메서드 이름을 나누어야 하는 것은 아니다.

단, 주의할 것은 메서드 정의는 메서드를 정의하고 있는 패키지와 동일한 패키지에 정의된 타입에 대해서만 정의할 수 있다.

즉, 우리의 패키지에서 다른 패키지들에 대한 타입의 메서드를 정의할 수 없다는 것이다.

int, string과 같은 내장 타입도 마찬가이다.

1. 메서드 정의하기

메서드 정의는 함수 정의와 유사한데, 유일한 차이점은 메서드는 이름 앞에 리시버 매개변수(receiver parameter)를 추가로 선언해주어야 한다는 것이다.

메서드 선언 시에는 함수의 매개변수와 함께 리시버 매개변수의 이름과 타입을 지정해주어야 한다.

다음의 예제를 보도록 하자

func (m MyType) sayHi(){
    fmt.Println("hi from", m)
}

또는

func (m *MyType) sayHi(){
    fmt.Println("hi from", m)
}

m는 리시버 매개변수의 이름이고, MyType은 리시버 매개변수의 타입이다.
이처럼 매개변수 선언 시에는 함수의 매개변수와 함께 리시버 매개변수의 이름과 타입을 지정해주어야 한다.

정의한 메서드를 호출하기 위해서는 메서드를 호출할 값 다음에 도트 연산자를 사용하여 메서드의 이름과 괄호을 붙여주면 된다. 메서드를 호출하고 있는 값을 메서드 리시버라고 한다.

value.sayHi()

위의 정의한 메서드를 이용하기 위해서 value라는 메서드 리시버로 메서드를 부르는 것이다.

그렇다면 문제가 되었던 위의 예제를 고쳐보자

package main

import "fmt"

type Coffee struct {
	name  string
	price int
}

type Tea struct {
	name  string
	price int
}

func (c Coffee) drink(coffee Coffee, money *int) {
	fmt.Println("i drink " + coffee.name)
	*money = *money - coffee.price
}

func (t Tea) drink(tea Tea, money *int) {
	fmt.Println("i drink " + tea.name)
	*money = *money - tea.price
}

func main() {
	totalMoney := 100
	coffee := Coffee{
		name:  "india",
		price: 10,
	}
	tea := Tea{
		name:  "korea",
		price: 20,
	}
	coffee.drink(coffee, &totalMoney)           // i drink india
	tea.drink(tea, &totalMoney)                 // i drink korea
	fmt.Println("remain money is ", totalMoney) // remain money is  70
}

이제 두 사용자 정의 타입 모두 drink 라는 메서드를 개인적으로 갖게 될 수 있는 것이다. drink 라는 메서드는 동일한 이름을 갖더라도 어느 쪽에 속했느냐에 따라 정의가 달라지기 때문에 문제가 되지 않는다.

2. 리시버 매개변수

그런데, 리시버 매개변수는 무슨 역할을 할까??
유추해보건데

coffee.drink(coffee, &totalMoney)

에서 coffee.drink()의 coffee는 메서드 리시버이고, 이 사용자 정의 타입 인스턴스가 바로 리시버 매개변수가 된다.

그렇다면 위의 예를 볼 때, 겹치는 부분이 있을 것이다. 즉, 매개변수로 받는 coffee 역시도 coffee 인스턴스를 받았기 때문이다.

때문에 메서드 매개변수로 받은 coffee 인스턴스를 없애고, 리시버 매개변수를 이용해 코드를 변경해보자

package main

import "fmt"

type Coffee struct {
	name  string
	price int
}

type Tea struct {
	name  string
	price int
}

func (c Coffee) drink(money *int) {
	fmt.Println("i drink " + c.name)
	*money = *money - c.price
}

func (t Tea) drink(money *int) {
	fmt.Println("i drink " + t.name)
	*money = *money - t.price
}

func main() {
	totalMoney := 100
	coffee := Coffee{
		name:  "india",
		price: 10,
	}
	tea := Tea{
		name:  "korea",
		price: 20,
	}
	coffee.drink(&totalMoney)           // i drink india
	tea.drink(&totalMoney)                 // i drink korea
	fmt.Println("remain money is ", totalMoney) // remain money is  70
}

리시버 매개변수로 기존 drink 매서드의 매개변수를 줄일 수 있었다. 이쯤되면 다른 언어에서 비슷한 키워드들이 있다. 바로 this와 self이다.

즉, 리시버 매개변수는 this와 self의 역할을 하고 있다고 볼 수 있는 것이다.
go는 this나 self와 같은 특별한 변수를 사용하지 않는다. 이들과 리시버 매개변수의 가장 큰 차이는 self나 this는 암묵적으로 설정되는 반면 리시버 매개변수는 명시적으로 선언한다는 것이다.

3. 포인터 리시버 매개변수

이번에는 coffee를 마시면 마실수록 가격이 내려가도록 해보자

package main

import "fmt"

type Coffee struct {
	name  string
	price int
}

func (coffee Coffee) drink(money *int) {
	fmt.Println("i drink " + coffee.name)
	*money = *money - coffee.price
	if coffee.price-10 >= 0 {
		coffee.price -= 10
	}

}

func main() {
	totalMoney := 1000
	coffee := Coffee{
		name:  "india",
		price: 100,
	}

	coffee.drink(&totalMoney)
	coffee.drink(&totalMoney)
	coffee.drink(&totalMoney)
	coffee.drink(&totalMoney)
	fmt.Println("money's price is ", coffee.price) // money's price is  100
}

그런데 가격이 안줄어들었다는 것을 알 수 있다. 왜 일까??
바로 리시버 매개변수 자체가 포인터가 아니기 때문이다. 즉, 일반 매개변수와 리시버 매개변수는 별다를 바가 없다는 것이다.

때문에 포인터형으로 바꾸어주고 다시 실험해보자

func (coffee *Coffee) drink(money *int) {
	fmt.Println("i drink " + coffee.name)
	*money = *money - coffee.price
	if coffee.price-10 >= 0 {
		coffee.price -= 10
	}

}

단지 위의 예제에서 drink 메서드의 리시버 매개변수의 타입을 포인터로 바꾸어주면 된다. 즉, 포인터 리시버 매개변수가 되는 것이다. 만약 setter를 사용하게 된다면 포인터 리시버 매개변수를 사용해야 될 것이다.

이를 통해 알 수 있는 것은, 리시버 매개변수로 포인터를 사용해도 메서드로 인식된다는 것이다.

0개의 댓글