본문 바로가기

갈아먹는 go [3] struct와 class는 무엇이 다른가?

들어가며

go 언어에서는 class가 없습니다만 oop 스타일로 프로그래밍을 할 수 있습니다. go의 struct는 참 다재다능해서 메서드도 붙일 수 있고, embedding으로 상속도 흉내낼 수 있고, 인터페이스도 구현할 수 있습니다. 그렇다면 go는 class도 없으면서 object oriented language일까요? 이에 대해서 go 공식 도큐먼트[1]에는 다음과 같이 나와있습니다.

 

go는 object oriented language일 수도 있고 아닐 수도 있다고 하네요. 핵심은 struct를 이용해서 oop 스타일로 코딩을 할 수는 있지만 결정적으로 object type 간에 위계 관계가 없다고 합니다. 때문에 subclassing과 같은 기능을 지원하지 않는다고 하네요. 이 부분이 저도 많이 헷갈렸는데요, struct와 class가 어떻게 다른지 직접 코드를 짜보면서 비교해보겠습니다. 

 

모든 소스코드는 다음 레포에서 확인 가능합니다.

https://github.com/yeomko22/go_basics/tree/master/ch3_struct

 

yeomko22/go_basics

go basic sample codes for practice. Contribute to yeomko22/go_basics development by creating an account on GitHub.

github.com

go struct embedding inheritance

먼저 go에서 struct embed를 사용해서 어떻게 상속을 흉내낼 수 있는지 살펴보겠습니다.

type Person struct {
	Name string
	Age  int
}

type Player struct {
	Person
	Team string
}

func (p *Person) Introduce() {
	fmt.Print("Hello, this is my info: ")
	p.Info()
}

func (p *Person) Info() {
	fmt.Printf("name: %s, age: %d\n", p.Name, p.Age)
}

func (p *Player) Info() {
	fmt.Printf("name: %s, age: %d, team: %s\n", p.Name, p.Age, p.Team)
}

func TestStruct(t *testing.T) {
	person := Person{"John", 30}
	player := Player{Person{"Lilard", 25}, "portland"}
	person.Info()
	player.Info()    
	person.Introduce()
	player.Introduce()
}

// 출력 결과
name: John, age: 30
name: Lilard, age: 25, team: portland
Hello, this is my info: name: John, age: 30
Hello, this is my info: name: Lilard, age: 25

사람을 나타내는 Person과 농구선수를 나타내는 Player 구조체를 만들었습니다. 그리고 Player 구조체 안에는 변수명을 설정하지 않고 Person 구조체를 포함시켰습니다. 이를 구조체 임베딩이라 부르며 상속과 비슷한 효과를 냅니다.

 

Person 구조체에는 Introduce, Info 두 메서드를 연결하였습니다. 그리고 Player에는 Info 메서드만 연결하였으며, 이는 Person의 Info를 오버라이딩하는 효과가 있습니다. person과 player 객체를 각각 만든 뒤, Info를 호출하면 각각 이름과 나이, player의 경우에는 team를 출력합니다.

 

다음으로 Introduce 메서드를 보겠습니다. 이는 Player 구조체에서 오버라이딩 되지 않았으며, Info 함수를 호출합니다. 이를 각각 person, player 객체에서 호출해보면 모두 이름과 나이 밖에 출력 하지 않습니다. class와 비교하기 위해서 위의 예제 코드를 자바로 구현해보겠습니다. 

class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    void info(){
        System.out.printf("name: %s, age: %d\n", this.name, this.age);
    }
    void introduce() {
        System.out.print("Hello, this is my info: ");
        this.info();
    }
}

class Player extends Person {
    String team;
    public Player(String name, int age, String team) {
        super(name, age);
        this.team = team;
    }
    void info() {
        System.out.printf("name: %s, age: %d, team: %s\n", this.name, this.age, this.team);
    }
}

public class main {
    public static void main(String[] args) {
        Person person = new Person("John", 30);
        Player player = new Player("Lilard", 25, "portland");
        person.info();
        player.info();
        person.introduce();
        player.introduce();
    }
}

// 출력 결과
name: John, age: 30
name: Lilard, age: 25, team: portland
Hello, this is my info: name: John, age: 30
Hello, this is my info: name: Lilard, age: 25, team: portland

 

 

맨 마지막에 player.introduce() 라인만 주목해서 보시면 됩니다. 같은 코드이지만 java에서는 player.introduce의 결과로 팀까지 출력이 되는 것을 확인할 수 있습니다. 바로 이러한 차이 때문에 go의 struct embedding을 활용하여 상속을 흉내낼 수 있지만 타입 간의 위계 관계를 나타내지 않습니다.

go struct subtyping

다음으로 struct의 한계점인 subtyping에 대해서 알아보겠습니다. 먼저 go 코드입니다.

func SayHello(p Person) {
	fmt.Printf("Hello %s\n", p.Name)
}

func TestStruct(t *testing.T) {
	person := Person{"John", 30}
	player := Player{Person{"Lilard", 25}, "portland"}
	SayHello(person)
	SayHello(player)
 
 // 출력 결과
 cannot use player (type Player) as type Person in argument to SayHello
 

다음은 동일한 로직을 java로 구현한 것입니다.

public class main {
    public static void SayName(Person p) {
        System.out.printf("Hello %s\n", p.name);
    }
    public static void main(String[] args) {
        Person person = new Person("John", 30);
        Player player = new Player("Lilard", 25, "portland");
        SayName(person);
        SayName(player);
    }
}
// 출력 결과
Hello John
Hello Lilard

보시면 go에서는 에러가 나는 반면 java에서는 제대로 출력이 됩니다. go에서는 아무리 Person을 임베딩으로 가지고 있는 구조체라 할 지라도 Person 구조체로 인정해주지 않습니다. 이를 통해서 go의 구조체 임베딩이 확실히 객체 간의 위계 관계를 나타내지 못한다는 것을 알 수 있습니다. 

마치며

지금까지 java와 go로 동일한 로직을 코딩해보면서 struct와 class가 어떻게 다른지 살펴보았습니다. 상속을 흉내낸다는 말이 무엇인지 명확하게 알지 못하였는데 속이 시원하네요 :) 앞선 subtyping을 go에서 구현하기 위해서는 interface를 이용해야 합니다. interface에 대해서는 다음 포스팅에서 알아보겠습니다. 

 

감사합니다.

Reference

[1] go document faq, https://golang.org/doc/faq

[2] object oriented inheritance in go, https://hackthology.com/object-oriented-inheritance-in-go.html#:~:text=Sharing%20Data%2C%20Code%20or%20Both&text=Syntactically%2C%20inheritance%20looks%20almost%20identical,fields%20of%20the%20parent%20struct%20.