The Ultimate Go Study Guide의 내용을 참고하여 작성했습니다.
Go 언어는 Class와 상속을 지원하지 않습니다. 대신 구조체 합성을 통해서 객체지향 프로그래밍을 흉내 낼 수 있습니다. 어느 정도 객체지향 프로그래밍의 장점을 사용할 수는 있지만 객체지향 프로그래밍을 위한 풍부한 언어적 지원을 한다고 보기는 어렵기 때문에 흉내 내기라고 표현해 봤습니다.
도형의 넓이를 계산하는 간단한 예제로 살펴보겠습니다. 우선 인터페이스를 하나 정의합니다. Area
는 면적을 계산하는 인터페이스입니다. Go 언어에서는 Java 같은 언어의 implements
키워드가 존재하지 않습니다. 즉, 인터페이스를 명시적으로 따르기 위한 문법이 존재하지 않습니다. 컴파일 시점에 특정 타입 T
의 메서드 집합에 인터페이스에서 정의한 함수 F(), F1()..
모두를 구현하고 있으면 암시적으로 인터페이스를 충족하는 것으로 결정됩니다.
package main type Area interface { Area() float64 }
shapes
패키지에는 여러 도형을 관리하는 패키지입니다.shape
구조체는 도형을 표현하는 구조체로 다른 도형들의 부모가 되는 추상(?) 클래스 역할을 합니다. <1>
에서 shape
구조체와 멤버 필드는 모두 패키지 내부에서만 접근 가능하도록 선언했습니다. 이렇게 하면 패키지 외부에서 shape
구조체를 생성할 수 없게됩니다.
<2>
에서 Area
인터페이스를 구현합니다. 리시버를 이용하여 shape
구조체의 메서드로 선언합니다. shape
구조체를 추상(?) 클래스라고 설명했지만 Go 언어에서는 모든 메서드를 구현해야만 하며 추상 클래스를 표현할 방법이 없습니다. 여기서는 넓이를 0으로 리턴합니다.
<3>
에서 fmt.Stringer
인터페이스를 구현합니다. 도형 객체의 타입, 이름, 넓이를 출력합니다.
package shapes import "fmt" // <1> type shape struct { name string } // <2> func (*shape) Area() float64 { return 0 } // <3> func (s *shape) String() string { return fmt.Sprintf("Type=[%T], Name=[%s], Area=[%f]", s, s.name, s.Area()) }
Rectangle
구조체는 사각형을 표현합니다. <1>
에서 shape
구조체를 임베딩합니다. shape
구조체는 내부 타입이 되고 Rectangle
구조체는 외부 타입이 됩니다. 구조체를 임베딩하면 내부 타입의 메서드는 외부 타입의 메서드로 호출할 수 있게 됩니다. 이를 내부 타입 승격이라고 합니다. 내부 타입 승격 시 메서드의 수신자는 외부 타입이 아닌 내부 타입이 됩니다.
<2>
에서 넓이와 높이 필드를 패키지 내부에서만 접근할 수 있도록 선언합니다. <3>
에서는 생성자 함수를 정의합니다. 생성자 함수는 New구조체명
네이밍 관례를 많이 사용합니다. 생성자 함수에서는 Rectangle
객체를 생성하여 참조를 반환합니다. 구조체의 멤버 필드인 w, h
는 생성자 함수를 통해서만 초기화할 수 있게됩니다. 필요 시 Getter, Setter를 추가하여 캡슐화의 장점을 누릴 수 있습니다. 하지만 같은 패키지 내부에서는 Getter, Setter를 통하지 않고 접근이 가능하므로 엄격한 의미에서 캡슐화되었다고 보기는 어렵습니다.
<4>
Area 인터페이스를 구현합니다. 도형이 면적을 계산하는 예제이므로 Area()
함수가 핵심 비즈니스 로직에 해당하며 각 도형의 면적을 계산하여 반환합니다. Rectangle
구조체의 Area()
메서드를 호출하면 shape
의 Area()
메서드가 아닌 Rectangle
의 Area()
메서드가 호출 됩니다. 여기서 착각하지 말아야 할 것은 이것이 메서드 오버라이딩은 아니라는 점입니다. 단순히 Rectangle
구조체도 독립적으로 Area
인터페이스를 구현하고 있을 뿐입니다.
package shapes import "fmt" type Rectangle struct { // <1> shape // <2> w float64 h float64 } // <3> func NewRectangle(w, h float64) *Rectangle { return &Rectangle{shape{"사각형"}, w, h} } // <4> func (s *Rectangle) Area() float64 { return s.w * s.h } func (s *Rectangle) String() string { return fmt.Sprintf("Type=[%T], Name=[%s], Area=[%f]", s, s.name, s.Area()) }
Triangle
은 삼각형, Circle
은 원을 표현하는 구조체입니다.
package shapes import "fmt" type Triangle struct { shape w float64 h float64 } func NewTriangle(w, h float64) *Triangle { return &Triangle{shape{"삼각형"}, w, h} } func (s *Triangle) Area() float64 { return s.w * s.h / 2 } func (s *Triangle) String() string { return fmt.Sprintf("Type=[%T], Name=[%s], Area=[%f]", s, s.name, s.Area()) }
package shapes import "fmt" const pi = 3.141592653589793238462643383279502884197169399375105820974944 type Circle struct { shape r float64 // 원의 반지름 } func NewCircle(r float64) *Circle { return &Circle{shape{"원"}, r} } func (s *Circle) Area() float64 { return s.r * s.r * pi } func (s *Circle) String() string { return fmt.Sprintf("Type=[%T], Name=[%s], Area=[%f]", s, s.name, s.Area()) }
내부 타입 승격을 확인해보기 위한 목적으로 아무것도 없는 도형인 Empty
구조체를 정의합니다. Empty
구조체는 shape
구조체를 임베딩할 뿐 Area
와 Stringer
인터페이스를 구현하지 않았습니다. 이렇게 하면 Empty
객체에 Area()
, String()
메서드를 호출하면 내부 타입 승격이 이루어지면서 내부 타입인 shape
구조체의 메서드가 호출됩니다. 당연히 메서드의 리시버도 shape
객체가 됩니다.
package shapes type Empty struct { shape } func NewEmpty() *Empty { return &Empty{shape{"도형"}} }
이제 도형 객체들을 이용해서 Go 언어가 제공하는 다형성과 내부 타입 승격을 확인해 보겠습니다. <1>
에서 다양한 도형들의 객체를 생성합니다. 생성한 객체는 Area
인터페이스 타입의 슬라이스인 someShapes
변수에 할당합니다. 인터페이스는 타입이므로 인터페이스를 만족하는 다른 타입을 위한 다형성 타입으로 사용이 가능합니다.
<2>
에서는 someShapes
슬라이스를 순회하면서 각 도형의 String()
, Area()
메서드를 호출합니다. 결과를 확인해보면 사각형, 삼각형, 원은 각 외부 타입에서 정의한 메서드가 호출된 것이 확인됩니다.
주목한 부분은 <3>
인데요, Empty
구조체는 Area
, Stringer
인터페이스를 구현하지 않았습니다. 이로 인해 Empty
객체에 대한 Area()
, String()
메서드 호출은 내부 타입 승격이 이루어져 shape
구조체의 메서드가 호출됩니다.
package main import ( "fmt" "hello-go/src/shapes" ) func main() { // <1> var someShapes = []Area{ shapes.NewRectangle(4, 5), shapes.NewTriangle(4, 5), shapes.NewCircle(5), shapes.NewEmpty(), } // <2> for _, s := range someShapes { fmt.Println(s, s.Area()) } } <출력> Type=[*shapes.Rectangle], Name=[사각형], Area=[20.000000] 20 Type=[*shapes.Triangle], Name=[삼각형], Area=[10.000000] 10 Type=[*shapes.Circle], Name=[원], Area=[78.539816] 78.53981633974483 Type=[*shapes.shape], Name=[도형], Area=[0.000000] 0 // <3>
착각하지 말아야 할 것은 임베딩을 통한 구현은 HAS-A 관계이며 IS-A 관계가 아니라는 점입니다. 즉 아래와 같이 사용 하는 것은 불가능 합니다.
var rect shape = NewRectangle(4, 5)
간단하게 Go 언어에서 객체지향 프로그래밍을 지원하는 기능을 살펴봤습니다. 엄밀한 의미에서 Go 언어는 객체지향 언어가 아닙니다. 객체지향 방법론을 이용하여 프로그래밍하기를 원한다면 Go 언어가 아닌 다른 언어를 선택하는게 바람직할 것 같습니다.