Go

Go의 메소드와 인터페이스

구루싸 2022. 1. 7. 23:46
반응형
SMALL

메소드

메소드 선언

메소드는 일반 함수 선언을 변형해 함수명 앞에 부가적인 파라미터를 추가한 형태로 선언한다. 파라미터는 함수를 파라미터 타입에 추가한다.

package geometry

import "math"

type Point struct{ X, Y float64 }
type Path []Point

// function
func Distance(p, q Point) float64 {
   return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// method
func (p Point) Distance(q Point) float64 {
   return math.Hypot(q.X-p.X, q.Y-p.Y)
}

func (path Path) Distance() float64 {
   sum := 0.0
   for i := range path {
      if i > 0 {
         sum += path[i-1].Distance(path[i])
      }
   }
   return sum
}

위의 코드에서 추가 파라미터 p를 메소드의 수신자라 부른다. Go에서는 수신자에 this나 self 등의 특별한 이름을 사용하지 않는다. 위의 두 Distance 함수 선언 간에는 충돌이 일어나지 않는다. 첫 번째는 패키지 수준 함수 geometry.Distance이고 두 번째는 Point 타입의 메소드를 선언하므로 이름이 Point.Distance가 된다. p.Distance 표현식은 Point 타입의 수신자 p에 대응하는 Distance 메소드를 선택하므로 셀렉터라 한다. Path는 Point와는 달리 구조체 타입이 아닌 명명된 슬라이스 타입이지만, 여기에도 메소드를 정의할 수 있다. Go는 다른 수많은 객체지향 언어들과는 다르게 어떤 타입에도 메소드를 붙일 수 있다. 메소드는 타입이 포인터나 인터페이스만 아니면 같은 패키지 내에 정의된 어떤 타입에도 선언할 수 있다.

포인터 수신자가 있는 메소드

함수를 호출하면 각 인자 값의 복사본이 생성되므로 함수에서 변수 값을 변경해야 하거나 인자가 커서 가급적 복사하고 싶지 않은 경우에는 포인터를 이용해 변수의 주소를 전달해야 한다. 

func (p *Point) ScaleBy(factor float64) {
   p.X *= factor
   p.Y *= factor
}

이 메소드의 이름은 (*Point).ScaleBy다. 메소드 포인터 수신자가 있다면 꼭 필요하지 않은 Point의 다른 모든 메소드에도 포인터 수신자가 있어야 한다.  명명된 타입 T에서 모든 메소드의 수신자가 T 타입이라면 해당 타입의 인스턴스를 안전하게 복사할 수 있다. 하지만 포인터 수신자가 하나라도 있다면 내부 불변성을 깨트릴 수 있으므로 T의 인스턴스 복사는 피해야 한다.

내장 구조체를 통한 타입 조합

import "image/color"

type ColoredPoint struct {
   Point
   Color color.RGBA
}

ColoredPoint를 세 필드를 갖는 구조체로 정의하는 대신 Point를 내장해서 X, Y 필드를 사용할 수 있게 했다. ColoredPoint의 필드 중 내장된 Point에서 추가된 필드를 Point에 대한 언급 없이 선택할 수 있다. 또한 내장된 Point 필드의 메소드는 ColoredPoint에 정의된 메소드가 없더라도 ColoredPoint 수신자 타입을 이용해 호출할 수 있다. 이는 클래스 기반 객체지향 언어에서 서브 클래스 또는 파생 클래스라거나 이 타입 간 연관 관계에서 ColoredPoint가 Point의 종류 중 하나라는 식('is a' 관계)이 아니라 ColoredPoint가 Point를 갖고 있고('has a' 관계) Point에서 승격된 두 개의 추가 메소드 Distance와 ScaleBy를 갖고 있는 위임의 형태로 부가적인 래퍼 메소드를 생성하게 지시하는 것이다. 구조체 타입은 두 개 이상의 익명 필드를 가질 수 있다. 

var cache = struct {
   sync.Mutex
   mapping map[string]string
} {
   mapping: make(map[string]string),
}
func Lookup(key string) string {
   cache.Lock()
   v := cache.mapping[key]
   cache.Unlock()
   return v
}

캡슐화

객체의 사용자가 객체의 변수나 메소드에 접근할 수 없는 경우 객체가 캡슐화돼 있다고 한다. 캡슐화는 때로는 정보 은닉이라고 불리며, 객체지향 프로그래밍의 핵심 요소다. Go에는 식별자의 가시성을 제어하는 단 한 가지 메커니즘이 있는데, 대문자로 시작하는 식별자는 정의된 패키지에서 노출되며 대문자가 아니면 노출되지 않는다. 캡슐화에는 세 가지 이점이 있다. 첫째, 사용자가 직접 객체 변수를 수정할 수 없으므로 일부 문장만으로 이 변수에 허용되는 값의 범위를 알 수 없다. 둘째, 구현의 세부 사항을 숨김으로써 사용자가 이후에 변경될 수 있는 부분에 의존하지 않게 해 설계자가 API 호환성을 유지하면서 구현을 자유롭게 진화시킬 수 있다. 셋째, 사용자가 객체의 변수를 임의로 변경할 수 없다. 

인터페이스

인터페이스 규약

인터페이스 타입은 다른 타입의 동작을 일반화하거나 추상화해서 표현한다. 인터페이스의 일반화를 통해 함수를 특정 구현의 세부 사항에 구애받지 않고 더 유연하고 융통성 있게 작성할 수 있다. 인터페이스는 추상 타입이다. 인터페이스는 값의 표현이나 내부 구조 또는 지원하는 기본 연산을 드러내지 않고 메소드 중 일부만을 보여준다. 객체지향 프로그래밍의 핵심적인 특징 중 대체 가능성이 있는데 이는 한 타입을 동일한 인터페이스를 충족하는 다른 타입으로 자유롭게 변경하는 것이다. 

인터페이스 타입

인터페이스 타입은 구상 타입이 해당 인터페이스의 인스턴스로 인식되기 위해 필요한 메소드들을 지정한다. 구조체 내장과 유사한 문법으로, 모든 메소드에 대한 약칭 역할을 하는 새 인터페이스를 정의할 수 있다. 이를 인터페이스 내장이라 한다. 

인터페이스 충족

타입 안에 인터페이스에서 요구하는 모든 메소드가 있으면 이 타입이 인터페이스를 충족한다고 한다. 빈 인터페이스 타입인 interface{} 타입처럼 메소드가 전혀 없으면 충족하는 타입에 대해 아무런 요구 조건도 없으므로 빈 인터페이스에는 어떤 값도 할당할 수 있다. 

인터페이스 값

개념적으로 인터페이스 타입의 값이나 인터페이스 값에는 두 개의 구성 요소인 구상 타입과 값이 있다. 이를 인터페이스의 동적 타입과 동적 값이라 한다. Go와 같은 정적 타입 언어에서는 타입이 컴파일 시의 개념이므로, 타입은 값이 아니다. 이 개념 모델에서 타입 설명자라는 일련의 값에는 각 타입에 대한 이름과 메소드 같은 정보가 있다. 인터페이스 값에서 타입 구성 요소는 그에 해당하는 타입 설명자로 표현된다. 

타입 단언

타입 단언은 인터페이스 값에 적용되는 연산이다. 구문적으로는 x.(T)의 형태며, x는 인터페이스 타입의 표현식이고, T는 단언 타입이다. 타입 단언은 피연산자의 동적 타입이 단언 타입과 일치하는지 확인한다. 

import (
   "errors"
   "syscall"
)

var ErrNotExist = errors.New("file does not exist")

func IsNotExist(err error) bool {
   if pe, ok := err.(*PathError); ok {
      err = pe.Err
   }
   return err == syscall.ENOENT || err == ErrNotExist
}

타입 변환

인터페이스는 두 가지 방식으로 사용한다. 첫 번째는 인터페이스의 메소드로 인터페이스를 충족하는 구상 타입의 유사도를 표현하고 구상 타입의 세부 구현과 고유 작업을 숨기는 방식이다. 두 번째는 다양한 구상 타입 값을 저장할 수 있는 인터페이스 값의 기능을 활용해 인터페이스를 이러한 타입들의 결합으로 간주하는 것이다. 객체지향 프로그래밍에서는 이 두 방식을 서브타입 다형성과 애드혹 다형성이라 한다. 

 

반응형
LIST