dev./golang

Ch 17) 메서드

minikuma 2025. 1. 7. 23:12

이번 포스팅은 Go 언어의 메서드에 알아보려고 한다.


17.1 메서드 선언


메서드를 선언하려면 리시버를 func 키워드와 함께 사용한다.

func (r Rabbit) info() int {
    return r.width * r.heigth
}
  • r Rabbit: 리시버
  • info(): 함수 이름

리시버를 통해 메서드의 타입을 알 수 있고, 구조체(r)는 매개변수처럼 사용이 가능하다. 리시버로 모든 로컬 타입이 가능한데 여기서 로컬 타입은 해당 패키지 안에서 type 키워드로 선언된 타입을 의미한다.

package main

import "fmt"

type account struct {
    balance int
}

// 일반 함수
func withdrawFunc(a *account, amount int) {
    a.balance -= amount
}

// 메서드
func (a *account) withdrawMethod(amount int) {
    a.balance -= amount
}

func main() {
    a := &account{100}    // balance 가 100인 포인트 변수, 구조체를 가르키고 있음
    withdrawFunc(a, 100)  // 100 - 100 = 0
    a.withdrawMethod(30)  // 0 - 30 = -30

    fmt.Printf("%d \n", a.balance)
}
-30
  • 메서드를 선언한다. func 키워드와 함수명 사이에 리시버가 있으면 메서드 없으면 함수이다.
  • 메서드를 호출한다. 일반 함수와 메서드는 동작은 같지만 호출 방법은 다르다. withdrawMethod()는 리시버 타입인 *account 타입에 속한다. 호출할 때 점(.) 연산자를 사용하여 호출이 가능하다.

 

17.1.1 별칭 리시버 타입


모든 로컬 타입이 리시버의 대상이 되기 때문에 별칭 타입 또한 리시버가 될 수 있다. 심지어 int 와 같은 내장 타입도 가능하다.

package main

import "fmt"

// 사용자 별칭 타입
type myInt int

// 별칭 타입을 리시버를 가지고 있는 메서드
func (a myInt) add(b int) int {
    return int(a) + b
}

func main() {
    var a myInt = 10
    fmt.Println(a.add(30))

    var b int = 20
    fmt.Println(myInt(b).add(50))
}
40
70
  • myInt 타입은 내장함수 int 타입의 별칭이다. 별칭 타입은 리시버의 타입과 동일하기 때문에 add() 메서드 호출이 가능하다.
  • b 변수는 int 타입이다. myInt 타입이 int 의 별칭 타입이지만 두 개의 타입은 다른 타입이다. myInt의 add() 메서드를 사용할 수 없다. 하지만 별칭 타입 간 타입 변환을 지원하고 있어 myInt 타입으로 변환 후 add() 메서드를 사용할 수 있다.

 

17.2 메서드는 왜 필요한가?


동작만 봤을 때는 메서드, 함수가 별 차이가 없는 거 같은데 왜 메서드가 필요한 걸까? 메서드는 함수의 다른 표현으로 생각할 수 있지만 중요한 차이가 있는데 "어디에 속하는가?" 즉 소속의 차이이다. 일반 함수 같은 경우는 어디에도 속해 있지 않다. 그냥 함수 이름으로 호출이 가능하다. 하지만 메서드는 리시버 타입에 속해 있다. 메서드는 메서드 리시버만 봐도 어떤 타입인지 알 수 있다는 의미이다. 우리가 타입을 데이터, 메서드를 기능으로 생각한다면 메서드는 데이터와 기능을 모두 가지고 있는 모습으로도 볼 수도 있다. 이것은 어떻게 보면 객체지향에서 말하는  "결합도를 낮추고 응집도를 높여야 한다."라는 결합도와 응집도의 이야기를 떠올릴 수 있을 것이다. 결국 Go 프로그램에서도 메서드를 사용하여 객체지향적인 프로그램이 가능하다.

 

17.2.1 객체지향:절차중심에서 관계 중심으로 변화


메서드가 등장하기 이전에는 절차 중심의 프로그래밍이였다. 어떤 순서로 실행할 지 정의하는 게 프로그램 코드였다. 나중에 메서드가 생기고 나서는 기능과 데이터를 묶어 단일 객체로써 동작하게 되었다. 여기서 객체는 데이터와 기능이 묶여 있는 타입을 의미하고 이 타입의 인스턴스는 객체 인스턴스이다. 이러한 여러 단일 객체들이 관계를 맺고 소통하는 게 점점 중요하게 되었고 프로그래밍의 패러다임은 변화하였다.

type Student struct {
    FirstName string
    LastName string
    Age int
}

func (s *Student) EnrollClass(c *Subject) {
    // do something
}

func (s *Student) SendReport(p *Professor, r *Report) {
    // do something
}

예를들어 EnrollClass, SendReport 메서드는 Student라는 리시버(구조체)가 선언되어 있다. 이제 두 메서드는 Student 타입에 속하게 되었고 기능별로 정의된 객체를 만들 수 있다. 결국 Student 구조체는 두 메서드와 모두 관계를 맺는 객체를 만들 수 있다.

 

17.3 포인트 메서드 vs 값 타입 메서드 🌟


리시버를 값 타입과 포인터 타입으로 정의할 수 있다.

package main

import "fmt"

type account struct {
    balance int
    firstName string
    lastName string
}

// 포인터 메서드
func (a1 *account) withdrawPointer(amount int) {
    a1.balance -= amount
}

// 값 타입 메서드
func (a2 account) widthdrawValue(amount int) {
    a2.balance -= amount
}

// 변경된 값을 반환하는 값 타입 메서드
func (a3 account) withdrawReturnValue(amount int) account {
    a3.balance -= amount
    return a3
}

func main() {
    var mainA *account = &account{100, "Joe", "Park"}
    mainA.withdrawPointer(30)  // 포인터 메서드 호출
    fmt.Println(mainA.balance) // 100 - 30 = 70

    mainA.widthdrawValue(20)   // 값 타입 메서드 호출
    fmt.Println(mainA.balance) // 70

    var mainB account = mainA.withdrawReturnValue(20)
    fmt.Println(mainB.balance) // 70 - 20 = 50

    mainB.withdrawPointer(30)  // 포인터 메서드 호출
    fmt.Println(mainB.balance) // 50 - 30 = 20
}
70
70
50
20
  • 포인트 메서드를 호출하면 포인터가 가르키고 있는 주소값이 복사된다. 반면에 값 타입 메서드는 리시버 타입의 모든 값이 복사 된다. (구조체 모든 필드 값이 복사) > 아래 그림에서 mainA와 a1은 같은 인스턴스를 가르키고 있어 하나의 변화는 다른 쪽에 변화를 줄 수 있다.

Tucker Go 프로그래밍 발췌 - 주소 복사

  • 값 타입 메서드를 호출하면 모든 값 (구조체의 모든 필드)이 복사된다.
    아래 그림에서 mainA와 a2는 서로 다른 인스턴스이다. 하나의 변화는 다른 쪽에 변화를 주지 않는다.

Tucker Go 프로그래밍 발췌 - 값 복사

값 타입 메서드를 사용하면 값의 변경이 예상대로 되지 않는다. 이 문제를 해결하기 위해서는 변경된 값을 다시 반환해야 한다. (a3, mainA, mainB 모두 다른 인스턴스지만 결과를 다시 반환하는 방법으로 해결 가능) 

Tucker Go 프로그래밍 - 해결 방안

이를 통해 중요한 사실을 알 수가 있는데, 포인터 메서드는 메서드 내부에서 리시버의 값을 변경할 수 있는 반면에 값 타입 메서드는 메서드 내부에서 리시버의 값을 변경할 수 없다. 결국 메서드 내부에서 리시버의 변경이 필요한 경우인가? 그렇지 않은 경우인가?에 따라 구분하여사용이 필요하다.

 

References


  • Tucker의 Go 언어 프로그래밍 2판, 공봉식 저