GoSuda

Go 인터페이스는 상속이 아니다

By Yunjin Lee
views ...

개요

Go 인터페이스는 동일한 인자와 반환값을 갖는 함수를 여러 구조체에서 쉽게 가질 수 있게 하지만, java의 extends 키워드처럼 그 내부 함수의 동작까지 적절히 연장하고 오버라이드하는 방식과는 다릅니다. Go의 구성적 코드 재사용을 제대로 이해해야만 상속과 헷갈리지 않겠지만, 처음부터 이론적으로 완벽한 이해를 하는 것은 어렵습니다. 실수하기 좋은 시나리오와 함께 알아봅시다.

자주 하는 실수

초심자 분들은 다음과 같은 실수를 하실 수 있습니다.

 1package main
 2import (
 3	"fmt"
 4	"strings"
 5)
 6
 7type Fruits interface {
 8	GetBrix() float64
 9	GetName() string
10	SetLabel()
11	GetLabel(string) string
12	PrintAll()
13}
14
15type Apple struct {
16	Label string
17	Name  string
18	Brix  float64
19}
20
21type Watermelon struct {
22	Label string
23	Name  string
24	Brix  float64
25}
26
27func (a *Apple) PrintAll() {
28	fmt.Printf("Fruit: %s, Label: %s, Brix: %v\n", a.Name, a.Label, a.Brix)
29}
30
31const (
32	NO_LABEL = "EMPTY LABEL"
33)
34
35func (a *Apple) SetLabel(lbl string) {
36	a.Brix 	= 14.5;
37	a.Name 	= "apple";
38	lbl_lower := strings.ToLower(lbl)
39	if strings.Contains(lbl_lower, a.Name) {
40		fmt.Println("Succeed: Label was ", lbl)
41		a.Label = lbl;
42	} else {
43		fmt.Println("Failed: Label was ", lbl)
44		a.Label = NO_LABEL;
45	}
46}
47
48func (w *Watermelon) SetLabel(lbl string) {
49	w.Brix = 10;
50	w.Name = "watermelon";
51	lbl_lower := strings.ToLower(lbl)
52	if strings.Contains(lbl_lower, w.Name) {
53		w.Label = lbl;
54	} else {
55		w.Label = NO_LABEL;
56	}
57}
58
59func main() {
60	fmt.Println("Inheritance test #1")
61	apple := new(Apple)
62	watermelon := apple
63	apple.SetLabel("Apple_1")
64	fmt.Println("Apple, before copied to Watermelon")
65	apple.PrintAll()
66	watermelon.SetLabel("WaterMelon_2")
67	fmt.Println("Apple, after copied to Watermelon")
68	apple.PrintAll()
69	fmt.Println("Watermelon, which inherited Apple's Method")
70	watermelon.PrintAll()
71}

이러한 코드는 Go가 전통적인 상속을 따른다고 착각하면 문제가 없어 보입니다. 하지만 이것의 출력 결과는 다음과 같습니다.

1Inheritance test #1
2Succeed: Label was  Apple_1
3Apple, before copied to Watermelon
4Fruit: apple, Label: Apple_1, Brix: 14.5
5Failed: Label was  WaterMelon_2
6Apple, after copied to Watermelon
7Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
8Watermelon, which inherited Apple's Method
9Fruit: apple, Label: EMPTY LABEL, Brix: 14.5

여기서 Go의 동작은 다만 명확해집니다.

1watermelon := apple

이 코드는 전혀 Apple을 그대로 Watermelon 클래스로 변환하지 않습니다. 다만 watermelon은 apple에 대한 포인터일 뿐입니다.

여기서 다시 강조하지만, Go는 전통적인 상속 개념을 따르지 않습니다.

이러한 오해를 한 상태에서 코드를 짠다면 무의미한 포인터 생성, 예기치 못한 타 구조체를 위한 함수 복사 등의 치명적 오류가 생깁니다.

그렇다면 모범적인 코드는 어떠할까요?

Go언어에서 적절한 예시

 1package main
 2import (
 3	"fmt"
 4	"strings"
 5)
 6
 7type Fruits interface {
 8	GetBrix() float64
 9	GetName() string
10	SetLabel()
11	GetLabel(string) string
12	PrintAll()
13}
14
15type BaseFruit struct {
16	Name  string
17	Brix  float64
18}
19
20type Apple struct {
21	Label string
22	Fruit BaseFruit
23}
24
25type Watermelon struct {
26	Label string
27	Fruit BaseFruit
28
29}
30
31func (b *BaseFruit) PrintAll() {
32	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
33}
34
35
36const (
37	NO_LABEL = "EMPTY LABEL"
38)
39
40func (a *Apple) SetLabel(lbl string) {
41	a.Fruit.Brix 	= 14.5;
42	a.Fruit.Name 	= "apple";
43	lbl_lower := strings.ToLower(lbl)
44	if strings.Contains(lbl_lower, a.Fruit.Name) {
45		fmt.Println("Succeed: Label was ", lbl)
46		a.Label = lbl;
47	} else {
48		fmt.Println("Failed: Label was ", lbl)
49		a.Label = NO_LABEL;
50	}
51	fmt.Printf("Fruit %s label set to %s\n", a.Fruit.Name, a.Label);
52	a.Fruit.PrintAll()
53}
54
55func (w *Watermelon) SetLabel(lbl string) {
56	w.Fruit.Brix = 10;
57	w.Fruit.Name = "Watermelon";
58	lbl_lower := strings.ToLower(lbl)
59	if strings.Contains(lbl_lower, w.Fruit.Name) {
60		w.Label = lbl;
61	} else {
62		w.Label = NO_LABEL;
63	}
64	fmt.Printf("Fruit %s label set to %s\n", w.Fruit.Name, w.Label);
65	w.Fruit.PrintAll()
66}
67
68func main() {
69	apple := new(Apple)
70	watermelon := new(Watermelon)
71	apple.SetLabel("Apple_1")
72	watermelon.SetLabel("WaterMelon_2")
73}

그러나, Go에서도 상속처럼 보이게 하는 것은 가능합니다. 익명 임베딩이라는 예시입니다. 이것은 내부 구조체를 이름 없는 구조체로 선언하면 가능합니다. 이러한 경우에는 하위 구조체의 필드들을 명시 없이 사용해도 그대로 접근이 가능합니다. 이렇게 하위 구조체의 필드를 상위 구조체로 승격하는 패턴을 이용하면 경우에 따라 가독성의 향상이 가능합니다. 그러나 하위 구조체를 명시적으로 보여줘야 하는 경우에는 사용하지 않기를 권장합니다.

 1package main
 2import (
 3	"fmt"
 4	"strings"
 5)
 6
 7type Fruits interface {
 8	GetBrix() float64
 9	GetName() string
10	SetLabel()
11	GetLabel(string) string
12	PrintAll()
13}
14
15type BaseFruit struct {
16	Name  string
17	Brix  float64
18}
19
20type Apple struct {
21	Label string
22	BaseFruit
23}
24
25type Watermelon struct {
26	Label string
27	BaseFruit
28
29}
30
31func (b *BaseFruit) PrintAll() {
32	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
33}
34
35
36const (
37	NO_LABEL = "EMPTY LABEL"
38)
39
40func (a *Apple) SetLabel(lbl string) {
41	a.Brix 	= 14.5;
42	a.Name 	= "apple";
43	lbl_lower := strings.ToLower(lbl)
44	if strings.Contains(lbl_lower, a.Name) {
45		fmt.Println("Succeed: Label was ", lbl)
46		a.Label = lbl;
47	} else {
48		fmt.Println("Failed: Label was ", lbl)
49		a.Label = NO_LABEL;
50	}
51	fmt.Printf("Fruit %s label set to %s\n", a.Name, a.Label);
52	a.PrintAll()
53}
54
55func (w *Watermelon) SetLabel(lbl string) {
56	w.Brix = 10;
57	w.Name = "Watermelon";
58	lbl_lower := strings.ToLower(lbl)
59	if strings.Contains(lbl_lower, w.Name) {
60		w.Label = lbl;
61	} else {
62		w.Label = NO_LABEL;
63	}
64	fmt.Printf("Fruit %s label set to %s\n", w.Name, w.Label);
65	w.PrintAll()
66}
67
68func main() {
69	apple := new(Apple)
70	watermelon := new(Watermelon)
71	apple.SetLabel("Apple_1")
72	watermelon.SetLabel("WaterMelon_2")
73}

이 예시에서는 이러한 차이점이 있습니다.

1w.PrintAll() // w.Friut.PrintAll()이 아닌, 이름 없는 구조체를 통한 자동 승격 호출

두 예제 모두 중요한 지점은 이러합니다.

  • main은 간소하게, 함수는 기능 별로
  • 다른 구조체라면 다른 객체를
  • 공유가 필요할 경우 내부 구조체를 사용

이와 같은 프로그래밍 철학에 어떠한 이점이 있을까요?

이점

  • 공유가 필요한 메서드와 아닌 것의 구별 명확
  • 개별 구조체, 메서드에 책임 소재 분리
  • 필요한 기능 명세에 따라 구조적으로 분리된 코드

처음에는 Go언어는 전통적인 OOP와 달라 익숙하지 않을 수 있으나, 익숙해지면 명시적인 프로그래밍이 가능합니다.

요약

  • 책임 소재를 고립시키자
  • 구조체 단위로 세부적으로 나누자
  • 메서드는 자바의 추상 클래스처럼 이해해선 안 된다
  • 명시적이고 구체적인 프로그래밍을 하자 Go언어는 전통적인 OOP 모델보다 간단명료하고 개별적으로 다뤄져야 합니다. 확장적이게 프로그래밍하기보다 단계적이고 구조적으로 분리하여 작성하도록 합시다.