티스토리 뷰
📺 시리즈
2023.10.02 - [Go/디자인 패턴] - [Go] SOLID in Go - SOLID란?
🐭 Go는 클래스와 객체 대신 값과 타입을 가지고 있다
Go는 클래스(class)와 객체(object)를 사용하는 대신 다음과 같이 구조체(struct)와 다른 타입을 기반으로 정의된 타입(type)을 사용합니다.
type Gopher struct { // 구조체: 필드들의 집합체
Name string
State GopherState
}
func (g Gopher) String() string {
return fmt.Sprintf("%s is %s", g.Name, g.State)
}
type GopherState int8 // 별칭 타입: int8을 기반으로 새로운 타입을 정의
const (
Awake GopherState = iota
Sleeping
Eating
)
func (s GopherState) String() string {
switch s {
case Awake:
return "Awake"
case Sleeping:
return "Sleeping"
case Eating:
return "Eating"
default:
return "Unknown"
}
}
구조체는 값 타입이다
여기서 '구조체나 객체나 비슷한 거 아니야?'하고 의문이 들 수 있습니다. 둘의 차이점은 객체는 참조 타입이고 구조체는 값 타입이라는 것입니다. 구조체는 값 타입이기 때문에 다른 변수에 할당하거나 함수 또는 메서드의 인수로 넘겨줄 때 복사되어 전달됩니다.
구조체는 복사된다
func main() {
g1 := Gopher{Name: "Genesis Gopher", State: Awake}
g2 := g1
g2.Name = "Geoffrey Gopher"
fmt.Println(g1)
fmt.Println(g2)
}
$ go run main.go
Genesis Gopher is Awake
Geoffrey Gopher is Awake
코드를 보시면 우선 Gopher 구조체의 인스턴스를 생성하여 g1에 할당했습니다. 이어서 g2에 g1을 할당하고 g2의 Name 필드를 변경했습니다. 이때 g2는 g1을 참조하는 것이 아닌 g1의 복사본을 가지게 됩니다. 따라서 g2의 Name 필드를 변경한다 한들, g1에게는 어떤 영향도 없습니다.
구조체를 매개변수로 받는 함수
func PrintGopher(g Gopher) {
g.Name = "Donald Gopher"
g.State = Eating
fmt.Println(g)
}
func main() {
g := Gopher{Name: "Genesis Gopher", State: Awake}
PrintGopher(g)
fmt.Println(g)
}
$ go run main.go
Donald Gopher is Eating
Genesis Gopher is Awake
함수에 구조체를 인수로 넘겨줄 때도 마찬가지로 복사되어 전달됩니다. 따라서 함수 안에서 적용된 변경사항들은 원본 구조체에 아무런 영향을 주지 않습니다.
구조체의 포인터를 매개변수로 받는 함수
func PrintGopherByRef(g *Gopher) {
g.Name = "Jeffrey Gopher"
g.State = Sleeping
fmt.Println(g)
}
func main() {
g := Gopher{Name: "Genesis Gopher", State: Awake}
PrintGopherByRef(&g)
fmt.Println(g)
}
$ go run main.go
Jeffrey Gopher is Sleeping
Jeffrey Gopher is Sleeping
구조체는 값 타입이지만 포인터를 사용하여 참조 타입으로 다룰 수도 있습니다. PrintGopherByRef 함수는 구조체의 포인터를 매개변수로 받습니다. 함수를 호출할 때 포인터도 복사되어 전달되기는 하지만, 함수 내부에서 포인터가 가리키는 구조체에 접근이 가능합니다. 따라서 함수 내부에서의 변경사항이 원본 구조체에서 적용되어 동일한 출력 결과를 얻게 됩니다.
모든 정의된 타입은 메서드를 가질 수 있다
값 타입 메서드
func (g Gopher) String() string {
return fmt.Sprintf("%s is %s", g.Name, g.State)
}
func (s GopherState) String() string {
switch s {
case Awake:
return "Awake"
case Sleeping:
return "Sleeping"
case Eating:
return "Eating"
default:
return "Unknown"
}
}
'GopherState'는 int8을 사용해 정의된 별칭 타입이고 Gopher는 구조체로, 이 또한 정의된 타입입니다. 따라서 위와 같이 메서드를 가질 수 있습니다. 메서드는 '(g Gopher)', '(s GopherState)'와 같이 func 키워드와 메서드명 사이에 들어가는 리시버를 사용하여 어떤 타입에 속해 있는지 명시되어야 합니다. 이때 리시버에 명시된 변수는 해당 메서드의 매개변수처럼 사용될 수 있으며 값이 복사되어 전달됩니다.
포인터 메서드
func (g *Gopher) SetName(name string) {
g.Name = name
}
func main() {
g := Gopher{Name: "Genesis Gopher", State: Awake}
g.SetName("Godfrey Gopher")
fmt.Println(g)
}
$ go run main.go
Godfrey Gopher is Awake
앞서 구조체의 포인터를 받아 함수 내부에서 구조체를 변경할 수 있었던 것처럼, 리시버를 포인터로 받게 되면 메서드 내부에서 구조체 또는 별칭 타입의 값을 변경할 수 있습니다.
🧩 메서드는 왜 필요한가?
동일한 동작을 하는 메서드와 함수
func (g *Gopher) SetName(name string) {
g.Name = name
}
func SetName(g *Gopher, name string) {
g.Name = name
}
메서드는 함수의 매개변수 하나를 리시버로 옮겨놓은 것이나 마찬가지입니다. 그러면 그냥 함수를 사용하면 되는데 왜 메서드 같은 메커니즘이 필요한 걸까요?
메서드는 타입에 '소속'되어 있다
func (g Gopher) Print() {
fmt.Println(g)
}
func main() {
g := Gopher{Name: "Genesis Gopher", State: Awake}
g.Print()
}
메서드가 필요한 이유는 '소속'입니다. 일반 함수는 어디에도 속하지 않지만, 메서드는 리시버가 가리키는 구조체 또는 타입에 소속됩니다. 예를 들어, 위와 같이 Gopher 구조체에 Print라는 메서드가 정의되어 있다면 어딘가에 있는 PrintGopher 함수를 찾아 헤맬 필요 없이 코드 에디터에 'g.'을 입력하기만 하면 Gopher 구조체의 메서드 목록이 뜨면서 손쉽게 필요한 메서드를 찾을 수 있습니다.
이처럼 메서드를 사용하면 타입과 관련된 기능들을 쉽게 추가하고 또 관련된 기능들을 한데 모아 관리할 수 있습니다. 즉, 코드의 응집도를 높일 수 있습니다. 코드의 응집도가 높아지면 새로운 기능을 추가하거나 변경이 필요할 때 전체 코드를 검토하고 변경할 필요 없이 관련된 코드의 부분만 변경하면 되므로, 기능 추가와 코드의 유지보수가 수월해집니다.
또한 구조체에 메서드를 추가함으로써 구조체는 단순한 데이터 묶음이 아닌, 기능을 가지고 동작하는 객체의 역할을 수행할 수 있게 됩니다. 결과적으로 Go는 클래스와 상속을 지원하지 않음에도 구조체와 메서드를 사용하여 객체 간의 상호관계를 표현함으로써 객체지향 프로그래밍을 가능케 합니다.
🙏 마치며
Go로도 객체지향 프로그래밍이 가능하다고는 하지만, 구조체와 메서드만으로는 설명이 부족한 것 같습니다. 이어지는 게시물에서는 인터페이스, 컴포지션(Composition), 패키지 등을 통해 Go의 객체지향 프로그래밍에 대해 더 깊게 알아보겠습니다.
📖 참고자료
글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!
'Go > 디자인 패턴' 카테고리의 다른 글
[Go] SOLID in Go - 단일 책임 원칙 (1) | 2023.10.11 |
---|---|
[Go] SOLID in Go - 패키지 (1) | 2023.10.10 |
[Go] SOLID in Go - 컴포지션 (1) | 2023.10.09 |
[Go] SOLID in Go - 인터페이스 (1) | 2023.10.04 |
[Go] SOLID in Go - SOLID란? (1) | 2023.10.02 |