프로그래밍 언어

[Things of Go] Go언어의 객체지향 모델 - 기초편

이 글은 독자가 객체지향과 포인터에 관한 기본적인 지식이 있다는 가정 하에 작성되었다.

Go언어와 객체지향

현재 인기있는 대부분의 프로그래밍 언어가 그렇듯이 Go언어는 객체지향 언어이다. 다만, 보통 객체지향에 입문하는 데에 쓰는 파이썬, 자바, 혹은 C++하고는 모습이 조금 다르다.

Go언어의 설계 목표가 직관성, 단순성, 명료성, 효율성이다. 이런 목표를 달성하기 위해 다른 언어들에 비해 키워드가 적고 문법이 살짝 다르다.

클래스

정의

Go언어에는 class 키워드가 없다. Go언어는 struct, 즉 구조체를 이용해서 클래스를 정의한다. 구조체는 다음과 같이 선언한다.

type Dog struct {
    name string
}

'string형 변수 name을 가지는 구조체를 Dog라 칭한다' 정도로 해석할 수 있다.

Go언어의 구조체는 다른 객체지향 언어의 구조체와 살짝 다르다. 다른 언어의 구조체와 마찬가지로 내부에 변수를 가지고 있으면서, 접근 제한이 가능하고, 메소드를 가지고 있다. Go언어에서 구조체와 클래스의 차이는 인스턴스를 만들어서 넘길 때에 있다. 구조체는 값을 복사해서 넘기지만 클래스는 그 값이 담긴 인스턴스의 주소를 넘긴다. 이에 관해선 나중에 설명하겠다.

문법

Go언어에서 구조체의 메소드는 다음과 같이 선언할 수 있다.

func (d Dog) Sounds() {
    fmt.Printf("%s : woof! woof!\n", d.name)
}

Go언어에서는 따로 접근 제한자 키워드가 없다. 대신 첫 문자가 대문자인지 소문자인지에 따라 접근 제한을 둔다. Go언어의 특이한 부분 중 하나는 이 접근 제한인데 같은 패키지 내에만 있으면 모든 클래스가 서로의 모든 변수 및 메소드를 사용할 수 있다. Go언어에서의 접근 제한은 패키지 밖에서 해당 패키지를 이용할 때의 접근 제한을 의미한다. 변수 및 메소드는 소문자로 시작하면 패키지 내부에서만 사용할 수 있고, 대문자로 시작하면 패키지 외부에서도 사용할 수 있다.

구조체와 클래스

돌아와서, Go언어에는 C언어 계열에서나 보던 포인터가 있다. 포인터의 의미와 상술한 내용을 연관지으면 struct 키워드 하나로 구조체와 클래스를 구분할 수 있다. 즉, 인스턴스를 만들 때 구조체는 struct 그대로, 클래스는 struct를 가리키는 포인터를 생성하면 된다. 예제를 보자.

package main

import "fmt"

type Dog struct {
    name string
}

type Cat struct {
    name string
}

func NewDog(name string) Dog {
    var d Dog
    d.name = name
    return d
} // 구조체

func (d Dog) Sounds() {
    fmt.Printf("%s: woof woof!\n", d.name)

}

func NewCat(name string) *Cat {
    var c Cat
    c.name = name
    return &c
}

func (c *Cat) Sounds() {
    fmt.Printf("%s: meow.\n", c.name)
} //클래스

func main() {
    d1 := NewDog("Baduk")
    d2 := d1
    d2.name = "Nureong"
    d1.Sounds()
    d2.Sounds()

    c1 := NewCat("Happy")
    c2 := c1
    c2.name = "Merry"
    c1.Sounds()
    c2.Sounds()

}
Baduk: woof woof!
Nureong: woof woof!
Merry: meow.
Merry: meow.

포인터를 알고 있다고 가정했기 때문에 위의 결과에 대해서는 따로 설명을 하지 않겠다. 특기할 만한 점으로 Go언어에서는 포인터의 메소드를 호출할 때 별도의 연산자 없이 .으로 호출하는 것이 있다.(C++의 경우 ->를 써야 한다)

서로 연관성이 적은 문법 요소(여기에서는 구조체와 포인터)를 합성하여 새로운 문법을 만들어내는 성질을 '직교성'이라고 하는데 언어의 직교성이 높을 수록 문법이 단순해지면서 유연해진다. 이 직교성이 높을 수록 문법이 단순하면서 유연해지는데, 이는 Go언어의 설계 목표에 부합한다.

인터페이스

정의

Golang에서 인터페이스는 자바의 인터페이스, C++의 추상 클래스 등과 비슷하다. 인터페이스는 아직 구현되지 않은 함수의 집합으로, interface 키워드를 이용해 정의한다. 인터페이스 내의 모든 함수를 구현한 객체는 특정 키워드 없이 자동적으로 인터페이스를 충족했다고 인식된다. 인터페이스는 다음과 같이 선언한다.

type Animal interface {
    Sounds()
}

문법

인터페이스 자체에는 특별한 문법이 없다. 단, 대부분의 언어들은 구조체 및 클래스에 인터페이스를 명시적으로 삽입해야 하지만

사용 및 존재 의의

객체지향에 익숙하지 않으면 인터페이스의 존재 이유에 의문이 생길 것이다. 하지만, 객체지향으로 쓰다 보면 깨닫게 된다. 인터페이스는 다형성의 꽃이다. 인터페이스를 충족하는 객체는 인터페이스의 인스턴스처럼 사용할 수 있다. (다른 말로 duck-typing 이라고도 한다. )예제를 보자.

package main

import "fmt"

type Animal interface {
    Sounds()
}

type Dog struct {
    name string
}

type Cat struct {
    name string
}

func NewDog(name string) Dog {
    var d Dog
    d.name = name
    return d
} // 구조체

func (d Dog) Sounds() {
    fmt.Printf("%s: woof woof!\n", d.name)

}

func NewCat(name string) *Cat {
    var c Cat
    c.name = name
    return &c
}

func (c *Cat) Sounds() {
    fmt.Printf("%s: meow.\n", c.name)
} //클래스

func SayHello (a Animal) {
    println("Hello!")
    a.Sounds()
}

func main() {
    var a Animal

    a = NewDog("Baduk")
    SayHello(a)

    a = NewCat("Happy")
    SayHello(a)

    SayHello(NewDog("Nureong"))
}
Hello!
Baduk: woof woof!
Hello!
Happy: meow.
Hello!
Nureong: woof woof!

객체지향의 목표 중 하나는 구현과 표현의 분리, 캡슐화이다.(은닉성도 이에 포함된다) 인터페이스는 구현과 표현의 분리를 도와준다. 한 객체를 설계할 때 인터페이스와 클래스 모두 이용할 수 있는 상황에서는 인터페이스를 이용하는 것이 좋다.

마치며...

이번 글에서는 Go언어에서 객체지향 모델을 이루는 요소들-구조체, 클래스, 인터페이스-에 대해서 알아보았다. 다음 글에서는 이 요소들을 객체지향적으로 이용하는 방법에 대해서 알아보도록 하겠다.

참고 자료

Golang Official Homepage, Documentation
위키백과, 객체 지향 프로그래밍, 2019.9.24
원유현, 프로그래밍 언어 개념, 정익사, 2011