프로그래밍 언어/Golang

Go: 구조체 (메서드, 임베딩, 캡슐화)

Go 에는 클래스가 없다. 개념적으로 객체라는 단어를 거의 사용하지 않는다. 다만 C언어처럼 구조체라는 존재가 있다. 구조체는 클래스와 유사하지만 전통적인 객체지향 프로그래밍의 형태보다는 Go 언어에 맞는 프로그래밍 방식이 요구된다. 객체지향에서 언급하는 상속캡슐화를 하는 것은 가능하지만 그 방법이 다른 언어와는 상당히 다르다.

 

기존 객체지향에서 사용하는 extends, public, protected, private 와 같은 키워드가 존재하지 않으며, this 키워드가 없어서 혼란스러울 일이 없다. 타언어를 사용하다보면(특히 자바스크립트) this 가 무엇을 가르키는지 소스코드에 명시가 되어있지 않기 때문에 헷갈리는 경우가 많은데, Go 에서는 명시하기 때문에 그럴 일이 없다.

구조체

구조체는 클래스처럼 객체를 찍어내기 위한 판이라고 생각하면 접근하기 좋다. struct 키워드를 사용하며 아래의 코드는 Blog 라는 구조체를 선언하고 main() 에서 구조체를 타입으로 갖는 변수를 선언하고 값을 할당하는 모습을 보여준다. 아래의 코드에서 구조체 타입을 가진 변수를 선언하고 할당하면서 행할 수 있는 방법들을 세가지 보여주고 있다. 각자 상황에 따라 쓰면 된다.

package main

type Blog struct {
	id    int
	name  string
	url   string
}

func main() {
	// Case 1
	var blog Blog
	blog.id = 1
	blog.name = "pronist"
	blog.url = "http://pronist.tistory.com"
	
	// Case 2
	blog = Blog{id: 1, name: "pronist", url: "http://pronist.tistory.com"}
	
	// Case 3
	blog = Blog{1, "pronist", "http://pronist.tistory.com"}
}

빈 구조체와 리터럴 타입

빈(Empty) 구조체와 구조체를 타입으로 선언하지 않고 구조체 리터럴 타입으로 사용하는 방법도 물론 있다. 위의 Blog 구조체를 리터럴 타입으로 하여 선언해보면 다음과 같다. 상당히 모양이 이상하다.

package main

import "fmt"

func main() {
	var blog = struct {
		id    int
		name  string
		url   string
	}{1, "pronist", "http://pronist.tistory.com"}

	// -> {1, pronist, http://pronist.tistory.com}
	fmt.Println(blog)
}

이것을 참고하여 리터럴을 사용하여 만든 빈 구조체는 아래와 같다.

package main

import "fmt"

func main() {
	// -> {}
	fmt.Println(struct{}{})
}

메서드

클래스에서 메서드를 만들 듯 해당 구조체에 속한 함수를 만들 수 있는데, 그것도 마찬가지로 메서드라고 한다. 다만 메서드를 만드는 방법이 특이한데, 일반적인 메서드 파라매터 이외에 리시버 파라매터(Reciver Parameter)라는 것을 사용한다. 이는 Go 가 가지는 특징 중 하나이다.

 

아래의 코드는 블로그에 글을 쓰는 것을 표현하는 write() 메서드이며 추가적으로 Post 구조체를 만들었다. 또한 Blog 구조체에 Post 구조체에 대한 배열을 만들었다.

package main

type Blog struct {
	// ...
	posts []*Post
}

type Post struct {
	title   string
	content string
}

func (b *Blog) write(p *Post) {
	b.posts = append(b.posts, p)
}

func main() {
	blog := Blog{1, "pronist", "http://pronist.tistory.com", []*Post{}}

	blog.write(&Post{"Go: Hello, World", "This is Golang"})
	blog.write(&Post{"Go: White in Go", "Java is verbose, Python is too slow"})
}

메서드의 표현을 보면 흥미롭게도 func 키워드 다음에 파라매터와 같은 형태로 나타낸 것을 볼 수 있는데, 저것이 바로 리시버 파라매터다. 저것의 역할은 this 키워드를 대신한다. 여러 의미를 가질 수 있는 this 에 비해 명시적으로 타입을 명시해 주는 것은 확실히 코드를 이해하기 더 좋은 듯하다. 

 

또한 저 코드에서 리시버 파라매터에 대해 포인터 자료형으로 주었는데, 포인터로 주지 않으면 값 복사가 되어 실제로 원본이 변화하지 않게된다. 따라서 포인터 리시버 파라매터를 주어 원본도 바뀔 수 있도록 한 것이다. 만약 다른 메서드를 선언할 때 원본의 값을 바꾸지 않는다고 하더라도 일관성을 위해 포인터 리시버 파라매터로 주는 것이 좋다. 즉, 포인터이거나 아니면 일반적인 경우이거나 둘 중에 하나로 통일하는 것이 좋다는 의미다.

package main

import "fmt"

// ...

func (b *Blog) Posts() []*Post {
	return b.posts
}

임베딩

구조체 임베딩은 클래스의 상속과 비슷하다고 생각하면 된다. 부모로부터 구조체 필드와 메서드를 상속받아서 사용할 수 있다. 하지만 상속이라고 표현하지 않고 위임이라고 하는 것이 좋다. 어떤 구조체 타입을 물려받은 것이라는 느낌은 아니기 때문이다. 아래의 코드는 Blog 구조체를 임베딩하여 만든 TistoryBlog 구조체다. 여기에 또한 User 구조체를 추가했다. TistoryBlog 구조체를 만들 때 기존에 있던 Blog 구조체의 이름을 그대로 입력하고 있음을 확인하면 된다.

 

이를 임베딩이라고 하는 이유는 구조체 안에 내부 구조체를 넣기 때문이다. 다만 TistoryBlog 구조체 필드에 있는 subscribers 변수처럼 사용하는 것과 구분해야 한다. 이처럼 구조체를 임베딩하면 상속을 표현할 수 있는데, Go 에서는 필드와 메서드가 승격된다고 표현한다.

package main

import "fmt"

// ...

type User struct {
	id    int
	email string
}

type TistoryBlog struct {
	subscribers []*User
	Blog
}

func (b *TistoryBlog) Subscribers() []*User {
	return b.subscribers
}

func (u *User) subscribe(b *TistoryBlog) {
	b.subscribers = append(b.subscribers, u)
}

func main() {
	tistoryBlog := TistoryBlog{
		[]*User{},
		Blog{1, "pronist", "http://pronist.tistory.com", []*Post{}},
	}
	helloPost := Post{"Go: Hello, World", "This is Golang"}

	tistoryBlog.write(&helloPost)

	user := User{1, "pronist@naver.com"}
	user.subscribe(&tistoryBlog)
    
	// -> [ {1, pronist@naver.com} ]
	for _, subscriber := range tistoryBlog.Subscribers() {
		fmt.Println(*subscriber)
	}
}

캡슐화

Go 에서도 캡슐화를 지원한다. 패키지 외부에서 필드에 대한 직접적 접근을 제한하고 Getter/Setter 메서드를 정의하는 것이다. 위에서 이미 Getter 메서드들은 정의를 해놓았다. Setter 만 만들면 된다. Go 에는 관련기능에 대한 문법을 언어 차원에서 지원하지 않는다. 자바스크립트에서 Getter/Setter 를 설정할 수 있겠지만, Go 에서는 메서드로 정의한다.

 

Go 의 캡슐화는 아주 간단하다. 메서드나 필드이름이 소문자이면 패키지 외부에서 접근할 수 없다. 하지만 대문자라면 가능하다. 위에서 작성한 코드의 필드는 모두 소문자이므로 외부 패키지에서 접근할 수 없다. 그러나 같은 패키지에서는 접근이 가능하다.

package blog

아래의 코드는 blog 패키지이며 main 패키지에서 접근 가능한 것은 대문자로 되어있는 메서드 뿐이다. 직접 필드접근은 불허한다. Setter 를 만들때 컨벤션상 Set* 의 형태로 한다.

package blog

type Blog struct {
	id    int
	name  string
	url   string
	posts []*Post
}

type Post struct {
	title   string
	content string
}

func NewBlog(id int, name string, url string, posts []*Post) *Blog {
	return &Blog{id, name, url, posts}
}

func NewPost(title string, content string) *Post {
	return &Post{title, content}
}

// ...Setters

func (b *Post) SetTitle(title string)  {
	b.title = title
}

// ...Getters

func (b *Blog) Posts() []*Post {
	return b.posts
}

// ...

func (b *Blog) Write(p *Post) {
	b.posts = append(b.posts, p)
}

package main

main 패키지에서 사용할 때는 다음과 같이 할 수 있다. New* 로 시작하는 것은 생성자라고 생각하면 좋다. 생성할 때 Go: Hello, world 라는 제목을 가지고 있으나, helloPost.SetTitle() 을 호출하여 Go: Hello, Go! 로 제목이 바뀌었다.

package main

import (
	"blog"
	"fmt"
)

func main() {
	b := blog.NewBlog(1, "pronist", "http://pronist.tistory.com", []*blog.Post{})
	helloPost := blog.NewPost("Go: Hello, world", "This is Golang")

	helloPost.SetTitle("Go: Hello, Go!")
	b.Write(helloPost)

	// -> [ {Go: Hello, Go!, This is Golang} ]
	for _, post := range b.Posts() {
		fmt.Println(*post)
	}
}