📖 이 글은 Saturday Night 스터디에서 The Ultimate Go를 주제로 발표하기 위해 만들어졌습니다.
OOP패턴 중 타입 계층(상속)에 대한 예시입니다. OOP를 기반으로 하는 언어 (C#, Java, Python등)를 다루는 개발자들은 기본적으로 타입 계층 형태로 구조를 만드는 것이 숙련되어 있는데요. 이전 스터디에서도 나온 이야기지만 아쉽게도 Go에서는 상속에 대한 동일한 수준의 기능을 제공하지 않습니다.
그래서 Go에서는 상속 개념을 활용하기 위해서는 접근 방식을 바꿔야만 합니다.
기존의 OOP는 공통 요소들을 묶어서
부모와 자식 형태로 설계를 하곤 합니다.
Go에 이러한 접근 방식으로 설계를 해봅시다.
이전 스터디 시간에 임베딩을 통해 상속을 흉내내본 적이 있었습니다. 아래에는 동물을 주제로 임베딩을 통해 OOP를 흉내내는 예제입니다.
type Animal struct {
Name string
IsMammal bool
}
func (a *Animal) Speak() {
fmt.Println("UGH!",
"My name is", a.Name,
", it is", a.IsMammal,
"I am a mammal.")
}
Animal이라는 구체적인 타입의 구조체가 존재하고 모든 동물에게 존재하는 이름과 포유류 여부를 판단하는 두개의 필드를 가지고 있습니다.
그리고 동물은 소리를 낼 수 있기 때문에 Speak라는 행동(메소드)를 정의했습니다.
부모 구조체를 만들었으니 자식 구조체를 만들어서 구체적인 동물을 설계해봅시다.
우선은 개를 설계해보죠.
type Dog struct {
Animal
PackFactor int
}
여기서 동물의 특성을 포함시키기 Animal 타입을 임베딩하고 PackFactor(무리짓는 특성...?)라는 Dog만이 가지는 고유한 필드를 정의하여 Dog 타입의 구조체를 정의했습니다.
Animal 타입을 재사용 했고 임베딩을 통해 상속을 흉내냈습니다.
func (d *Dog) Speak() {
fmt.Println("Woof!",
"My name is", d.Name,
", it is", d.IsMammal,
"I am a mammal with a pack factor of", d.PackFactor)
}
개는 말할 수 있는 동물이기 때문에 Animal 타입의 Speak 메소드를 오버라이딩하여 Speak을 구현했습니다.
이번엔 고양이 타입을 만들어봅시다.
type Cat struct {
Animal
ClimbFactor int
}
Dog와 마찬가지로 Animal 타입을 임베딩하고 Cat만이 가지는 필드를 정의하여 구조체를 정의했습니다.
func (c *Cat) Speak() {
fmt.Println("Meow!",
"My name is", c.Name,
", it is", c.IsMammal,
"I am a mammal with a climb factor of", c.ClimbFactor)
}
고양이도 마찬가지로 말을 할 수 있기 때문에 Speak를 오버라이딩하여 구현했습니다.
func main() {
dog := animals.Dog{
Animal: animals.Animal{ Name: "Brian", IsMammal: true },
PackFactor: 5,
}
cat := animals.Cat{
Animal: animals.Animal{ Name: "Navi", IsMammal: true },
ClimbFactor: 4,
}
dog.Speak()
cat.Speak()
}
main 함수에서 구조체 리터럴을 통해 임베딩된 Animal 필드와 PackFactor, ClimbFactor 필드를 초기화하여 dog와 cat을 생성했습니다. 그리고 dog 값과 cat 값을 통해 각각 Speak 메소드를 호출했습니다.
이는 정상적으로 컴파일되며 호출이 됩니다.
위의 예제에서 임베딩을 활용하여 상속을 구현했지만 Go에서는 서브타이핑의 개념을 지원하지 않으며, 위와 같은 설계 방식에는 문제가 있습니다.
위 예제에서 Animal의 기본 타입으로 Dog와 Cat을 사용할 수가 없습니다.
Animal을 기준으로 Dog와 Cat을 그룹화할 수 없다는 뜻입니다.
// Attempt to use Animal as a base type.
animals := []Animal{
Dog{},
Cat{},
}
// COMPILE ERROR
: cannot use Dog literal (type Dog) as type Animal in array or slice literal
: cannot use Cat literal (type Cat) as type Animal in array or slice literal
Animal 타입으로 Dog와 Cat타입을 배열이나 슬라이스로 선언하려고하면 컴파일러는 Dog와 Cat타입이 Animal 타입으로 사용할 수 없다고 에러를 내뿜습니다.
이런 특징들 때문에 Go는 공통된 필드를 바탕으로 그룹핑하여 설계하는 것을 권장하지 않습니다.
이번에는 Go에 권장되는 형태로 만들어 봅시다.
위의 구조에서 인터페이스를 활용해 공통 요소가 아니라 공통 행동으로 그룹화하는 접근 방식으로 구현해봅시다.
Speaker 라는 공통된 행위를 의미하는 인터페이스를 생성합니다.
type Speaker interface {
Speak()
}
이제 Dog와 Cat은 Speaker라는 행위에 충족하는 메소드를 만들면 됩니다.
type Dog struct {
Name string
IsMammal bool
PackFactor int
}
func (d Dog) Speak() {
fmt.Println("Woof!",
"My name is", d.Name,
", it is", d.IsMammal,
"I am a mammal with a pack factor of", d.PackFactor)
}
type Cat struct {
Name string
IsMammal bool
ClimbFactor int
}
func (c Cat) Speak() {
fmt.Println("Meow!",
"My name is", c.Name,
", it is", c.IsMammal,
"I am a mammal with a climb factor of", c.ClimbFactor)
}
각 Dog와 Cat은 중복되는 필드를 각각 정의하여 중복 작성하는 행위를 유발시키지만 이러한 디커플링은 코드 재사용성보다 더 나은 방법일 수도 있습니다.
왜 이런 형태로 구현하는게 더 나은 방법일까요? 이전 예제는 다음과 같았습니다.
Go에서 타입을 선언하기 위한 가이드라인은 다음과 같습니다.
- Declare types that represent something new or unique.
- Validate that a value of any type is created or used on its own.
- Embed types to reuse existing behaviors you need to satisfy.
- Question types that are an alias or abstraction for an existing type.
- Question types whose sole purpose is to share common state.
참고
아 영어 어렵다. ^^ 🤯
다시 돌아와서 Speaker로 그룹핑을 하여 Dog와 Cat이 짖도록 할 수 있습니다.
func main() {
spkrs := []animals.Speaker{
animals.Dog{
Name: "Brian",
IsMammal: true,
PackFactor: 5,
},
animals.Cat{
Name: "Navi",
IsMammal: true,
ClimbFactor: 4,
},
}
for _, spkr := range spkrs {
spkr.Speak()
}
}
Speaker 인터페이스 슬라이스로 Dog와 Cat을 그룹화하였고 슬라이스를 반복하여 Dog와 Cat이 짖을 수 있도록 했습니다.
행동에 의한 그룹화에 대한 결론은 다음과 같습니다.
한글로 번역된 ulitmate-go 문서의 내용은 번역한 내용이 이해가 잘 안가서 이 글을 더 참고했습니다.