Go의 메서드 사용

완료됨

Go의 메서드는 특수한 형식의 함수로, 함수 이름 앞에 추가 매개 변수를 포함해야 한다는 점에서 약간 차이가 있습니다. 이 추가 매개 변수를 수신자라고 합니다.

메서드는 함수를 그룹화하여 사용자 지정 형식에 연결하려는 경우에 유용합니다. Go의 이 방식은 다른 프로그래밍 언어에서 클래스를 만드는 것과 유사한데, 이 방법을 통해 포함, 오버로드, 캡슐화 같은 OOP(개체 지향 프로그래밍) 모델의 특정 기능을 구현할 수 있기 때문입니다.

Go에서 메서드가 중요한 이유를 이해하기 위해 메서드를 선언하는 방법부터 시작해 보겠습니다.

메서드 선언

지금까지 Go에서 만들 수 있는 다른 사용자 지정 형식으로 구조체만 사용했습니다. 이 모듈에서는 메서드를 추가하여 직접 만든 구조체에 동작을 추가하는 방법을 알아봅니다.

메서드를 선언하는 구문은 다음과 같습니다.

func (variable type) MethodName(parameters ...) {
    // method functionality
}

그러나 메서드를 선언하려면 먼저 구조체를 만들어야 합니다. 기하 도형 패키지를 만들고 해당 패키지의 일부로 triangle이라는 삼각형 구조체를 만들려고 합니다. 그런 다음 메서드를 사용하여 해당 삼각형의 둘레를 계산하려고 합니다. Go에서 다음과 같이 나타낼 수 있습니다.

type triangle struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

일반적인 구조체처럼 보이지만 perimeter() 함수의 함수 이름 앞에 triangle 형식의 추가 매개 변수가 있습니다. 수신자는 구조체를 사용할 때 다음과 같이 함수를 호출할 수 있음을 뜻합니다.

func main() {
    t := triangle{3}
    fmt.Println("Perimeter:", t.perimeter())
}

일반적인 방식으로 perimeter() 함수를 호출하려고 하면 함수의 서명에 수신자가 필요하다고 표시되기 때문에 작동하지 않습니다. 해당 메서드를 호출하는 유일한 방법은 구조체를 먼저 선언하여 메서드에 대한 액세스 권한을 제공하는 것입니다. 메서드가 다른 구조체에 속하는 한 동일한 이름을 지정할 수도 있습니다. 예를 들면 다음과 같이 perimeter() 함수를 사용하여 square 구조체를 선언할 수 있습니다.

package main

import "fmt"

type triangle struct {
    size int
}

type square struct {
    size int
}

func (t triangle) perimeter() int {
    return t.size * 3
}

func (s square) perimeter() int {
    return s.size * 4
}

func main() {
    t := triangle{3}
    s := square{4}
    fmt.Println("Perimeter (triangle):", t.perimeter())
    fmt.Println("Perimeter (square):", s.perimeter())
}

위의 코드를 실행하면 오류가 없으며 다음과 같이 출력됩니다.

Perimeter (triangle): 9
Perimeter (square): 16

perimeter() 함수에 대한 두 호출 중에서 컴파일러는 수신자 형식에 따라 호출할 함수를 결정합니다. 이렇게 하면 패키지의 함수에서 일관성 및 짧은 이름을 유지할 수 있으며 패키지 이름을 접두사로 포함하지 않아도 됩니다. 다음 단원에서 인터페이스를 검토할 때 이 점이 중요한 이유를 살펴보겠습니다.

메서드의 포인터

메서드가 변수를 업데이트해야 하는 경우가 있습니다. 또는 메서드에 대한 인수가 너무 크면 복사하지 않는 것이 좋을 수도 있습니다. 이러한 경우에는 포인터를 사용하여 변수의 주소를 전달해야 합니다. 이전 모듈에서 포인터에 관해 설명할 때는 Go에서 함수를 호출할 때마다 Go에서 각 인수 값의 복사본을 만들어 사용한다고 했습니다.

메서드에서 수신자 변수를 업데이트해야 하는 경우에도 동일한 동작이 수행됩니다. 예를 들어 삼각형 크기를 두 배로 늘리는 새로운 메서드를 만들려고 합니다. 다음과 같이 수신자 변수에서 포인터를 사용해야 합니다.

func (t *triangle) doubleSize() {
    t.size *= 2
}

다음과 같이 메서드가 작동하는 것을 입증할 수 있습니다.

func main() {
    t := triangle{3}
    t.doubleSize()
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter:", t.perimeter())
}

위의 코드를 실행하면 다음과 같이 출력됩니다.

Size: 6
Perimeter: 18

메서드가 단순히 수신자의 정보에 액세스하는 경우에는 수신자 변수에 포인터가 필요하지 않습니다. 그러나 Go 규칙에 따르면 구조체의 메서드에 포인터 수신자가 있는 경우 해당 구조체의 모든 메서드에 포인터 수신자가 있어야 합니다. 구조체의 메서드가 필요하지 않더라도 그렇습니다.

다른 형식에 대한 메서드 선언

메서드의 한 가지 중요한 측면은 구조체와 같은 사용자 지정 형식만이 아니라 모든 형식에 대해 메서드를 정의한다는 점입니다. 그러나 다른 패키지에 속한 형식에서 구조체를 정의할 수는 없습니다. 따라서 string 같은 기본 형식에는 메서드를 만들 수 없습니다.

하지만 hack을 사용하여 기본 형식에서 사용자 지정 형식을 만든 다음 기본 형식인 것처럼 사용할 수 있습니다. 예를 들어 소문자에서 대문자로 문자열을 변환하는 메서드를 만들려고 합니다. 다음과 같이 작성할 수 있습니다.

package main

import (
    "fmt"
    "strings"
)

type upperstring string

func (s upperstring) Upper() string {
    return strings.ToUpper(string(s))
}

func main() {
    s := upperstring("Learning Go!")
    fmt.Println(s)
    fmt.Println(s.Upper())
}

위의 코드를 실행하면 다음과 같이 출력됩니다.

Learning Go!
LEARNING GO!

처음으로 값을 출력할 때의 문자열인 것처럼 새 개체 s를 사용할 수 있습니다. 그런 다음, Upper 메서드를 호출하면 s는 형식 문자열의 모든 대문자를 출력합니다.

메서드 포함

이전 모듈에서는 한 구조체에서 속성을 사용하면서 동일한 속성을 다른 구조체에 포함할 수 있다는 것을 학습했습니다. 즉, 한 구조체의 속성을 다시 사용하면 반복을 방지하고 코드베이스에서 일관성을 유지할 수 있습니다. 메서드에도 유사한 개념이 적용됩니다. 수신자가 다른 경우에도 포함된 구조체의 메서드를 호출할 수 있습니다.

예를 들어 색을 포함하는 논리를 사용하여 새 삼각형 구조체를 만들려고 합니다. 또한 이전에 선언한 삼각형 구조체를 계속 사용하려고 합니다. 그러면 색이 지정된 삼각형 구조체는 다음과 같이 표시됩니다.

type coloredTriangle struct {
    triangle
    color string
}

그러면 다음과 같이 coloredTriangle 구조체를 초기화한 다음, triangle 구조체에서 perimeter() 메서드를 호출하고 해당 필드에 액세스할 수도 있습니다.

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

계속해서 위의 변경 내용을 프로그램에 포함하여 포함이 어떻게 작동하는지 확인합니다. 위 예제와 같이 main() 메서드를 사용하여 프로그램을 실행하면 다음과 같이 출력됩니다.

Size: 3
Perimeter 9

Java 또는 C++ 같은 OOP 언어에 대해 잘 알고 있는 경우 triangle 구조체는 기본 클래스처럼 보이고 coloredTriangle은 하위 클래스(예: 상속)라고 생각할 수 있지만 그렇지 않습니다. 실제로는 Go 컴파일러에서 래퍼 메서드를 만들어 perimeter() 메서드의 수준을 올리는 것이며, 다음과 같이 표시됩니다.

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

수신자는 삼각형 필드에서 perimeter() 메서드를 호출하는 coloredTriangle입니다. 다행히 위의 메서드를 만들 필요는 없습니다. 물론 만들 수 있지만 Go에서 내부적으로 만듭니다. 위 예제는 학습 목적으로만 포함되었습니다.

메서드 오버로드

앞에서 설명한 triangle 예제로 돌아가 보겠습니다. coloredTriangle 구조체에서 perimeter() 메서드의 구현을 변경하려고 하면 어떻게 될까요? 두 개의 함수에 동일한 이름을 사용할 수 없습니다. 그러나 메서드에는 추가 매개 변수(수신자)가 필요하기 때문에 사용하려는 수신자와 관련이 있는 한 이름이 같은 메서드를 사용할 수 있습니다. 이러한 구분을 사용하여 메서드를 오버로드합니다.

즉, 동작을 변경하려는 경우 방금 설명한 래퍼 메서드를 작성할 수 있습니다. 색이 지정된 삼각형의 둘레가 일반적인 삼각형 둘레의 두 배인 경우 코드는 다음과 같습니다.

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

이제 앞에서 작성한 main() 메서드에서 다른 항목을 변경하지 않으면 다음과 같이 표시됩니다.

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

이 코드를 실행하면 다음과 같이 출력이 달라집니다.

Size: 3
Perimeter 18

그러나 triangle 구조체에서 perimeter() 메서드를 호출해야 하는 경우 다음과 같이 명시적으로 액세스하여 수행할 수 있습니다.

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter (colored)", t.perimeter())
    fmt.Println("Perimeter (normal)", t.triangle.perimeter())
}

이 코드를 실행하면 다음과 같이 출력됩니다.

Size: 3
Perimeter (colored) 18
Perimeter (normal) 9

확인한 대로 Go에서 메서드를 ‘재정의’하고 필요한 경우 ‘원본’ 메서드에 계속 액세스할 수 있습니다.

메서드의 캡슐화

캡슐화는 개체의 호출자(클라이언트)가 메서드에 액세스할 수 없음을 의미합니다. 일반적으로 다른 프로그래밍 언어에서는 메서드 이름 앞에 private 또는 public 키워드를 배치합니다. Go에서 메서드를 퍼블릭으로 설정하려면 대문자로 된 식별자만 사용하고 프라이빗으로 설정하려면 대문자로 되지 않은 식별자만 사용해야 합니다.

Go의 캡슐화는 패키지 간에만 적용됩니다. 즉, 패키지 자체가 아닌 다른 패키지의 구현 세부 정보만 숨길 수 있습니다.

사용해 보려면 다음과 같이 새 패키지 geometry를 만들고 삼각형 구조체를 이 패키지로 이동합니다.

package geometry

type Triangle struct {
    size int
}

func (t *Triangle) doubleSize() {
    t.size *= 2
}

func (t *Triangle) SetSize(size int) {
    t.size = size
}

func (t *Triangle) Perimeter() int {
    t.doubleSize()
    return t.size * 3
}

다음과 같이 위의 패키지를 사용할 수 있습니다.

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Perimeter", t.Perimeter())
}

다음과 같이 출력됩니다.

Perimeter 18

main() 함수에서 size 필드 또는 doubleSize() 메서드를 호출하려고 하면 프로그램은 다음과 같이 패닉 상태가 됩니다.

func main() {
    t := geometry.Triangle{}
    t.SetSize(3)
    fmt.Println("Size", t.size)
    fmt.Println("Perimeter", t.Perimeter())
}

위의 코드를 실행하면 다음 오류가 표시됩니다.

./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)