
Go 언어, 상속 대신 '조합'으로 더 유연하게! 구조체 상속 완벽 이해하기
들어가며: Go 언어는 왜 상속 대신 조합을 선택했을까요?
많은 객체지향 프로그래밍 언어에서 '상속'이라는 개념은 정말 중요합니다.
마치 부모님의 좋은 유전자를 물려받는 것처럼, 새로운 클래스가 기존 클래스의 특징과 능력을 이어받을 수 있게 해주는데요.
하지만 Go 언어는 조금 다른 길을 선택했습니다.
전통적인 클래스 기반 상속 대신, '조합(Composition)'이라는 방식을 사용하는데요.
이번 글에서는 Go 언어의 구조체 조합이 어떻게 작동하는지, 그리고 이를 통해 어떻게 복잡한 데이터 구조를 만들어낼 수 있는지 쉽고 재미있게 알아보겠습니다.
Go 언어의 매력덩어리, 구조체(Struct)란 무엇일까요?
Go 언어에서 구조체(Struct)는 여러 변수들을 하나의 이름 아래 묶어주는 복합 데이터 타입입니다.
마치 필통 안에 여러 종류의 필기구를 담아두는 것과 비슷한데요.
이 필통 안의 필기구들처럼, 구조체 안의 변수들은 '필드(Field)'라고 불리며, 각각 다른 데이터 타입을 가질 수 있습니다.
간단한 예를 한번 살펴볼까요?
package main
import "fmt"
// 사람을 나타내는 구조체 정의
type Person struct {
Name string // 이름 필드 (문자열 타입)
Age int // 나이 필드 (정수 타입)
}
func main() {
// Person 구조체 변수 생성 및 초기화
p := Person{Name: "앨리스", Age: 30}
fmt.Println(p) // 생성된 Person 구조체 변수 출력
}
이 코드에서 Person이라는 구조체는 Name(이름)과 Age(나이)라는 두 개의 필드를 가지고 있습니다.
정말 간단하죠?
상속보다는 조합! Go 언어가 택한 현명한 길
Go 언어는 '상속보다는 조합을 사용하라(Composition over Inheritance)'는 프로그래밍 원칙을 적극적으로 따르고 있습니다.
복잡한 계층 구조를 가진 클래스를 만들기보다는, 단순한 타입들을 결합하여 더 복잡한 타입을 만드는 방식을 선호하는데요.
이것이 바로 구조체를 다른 구조체 안에 '포함(Embedding)'시키는 방식으로 구현됩니다.
구조체 안에 구조체를 쏘옥! 임베딩(Embedding) 파헤치기
하나의 구조체를 다른 구조체 안에 포함시키면, 마치 안쪽 구조체의 필드와 메소드(기능)들이 바깥쪽 구조체의 것처럼 자연스럽게 사용될 수 있습니다.
이게 어떻게 가능한지 코드를 통해 자세히 살펴보겠습니다.
package main
import "fmt"
// 주소를 나타내는 구조체 정의
type Address struct {
City, State string // 도시와 주 필드 (문자열 타입)
}
// 사람을 나타내는 구조체 정의
type Person struct {
Name string
Age int
Address // Address 구조체를 Person 구조체 안에 포함 (임베딩)
}
func main() {
// Person 구조체 변수 생성 및 초기화
p := Person{
Name: "밥",
Age: 25,
Address: Address{ // 포함된 Address 구조체 초기화
City: "뉴욕",
State: "NY",
},
}
fmt.Println(p)
// 포함된 구조체의 필드에 직접 접근 가능
fmt.Println("도시:", p.City)
}
위 코드에서 Person 구조체는 Address 구조체를 '임베딩'하고 있습니다.
이게 무슨 의미냐고요?Person 구조체가 Address 구조체의 필드들, 즉 City와 State를 마치 자신의 것처럼 직접 사용할 수 있게 된다는 뜻입니다.p.City처럼 바로 접근하는 모습을 볼 수 있죠?
이것이 바로 Go 언어에서 상속과 유사한 효과를 내는 방법입니다.
메소드도 내 것처럼! 메소드 승격(Method Promotion)의 마법
구조체를 임베딩하면 필드뿐만 아니라, 임베드된 구조체에 정의된 메소드들도 바깥쪽 구조체로 '승격(Promotion)'됩니다.
이게 무슨 말이냐고요?
바깥쪽 구조체가 마치 자신의 메소드인 것처럼 임베드된 구조체의 메소드를 호출할 수 있게 된다는 건데요.
백문이 불여일견! 코드로 확인해보겠습니다.
package main
import "fmt"
// 주소를 나타내는 구조체 정의
type Address struct {
City, State string
}
// Address 구조체의 메소드 정의: 전체 주소를 반환
func (a Address) FullAddress() string {
return a.City + ", " + a.State
}
// 사람을 나타내는 구조체 정의
type Person struct {
Name string
Age int
Address // Address 구조체 임베딩
}
func main() {
// Person 구조체 변수 생성 및 초기화
p := Person{
Name: "찰리",
Age: 28,
Address: Address{
City: "로스앤젤레스",
State: "CA",
},
}
// 임베드된 구조체의 메소드를 Person 구조체 변수를 통해 직접 호출
fmt.Println(p.FullAddress())
}
여기서 FullAddress는 분명 Address 구조체의 메소드인데, Person 구조체 변수인 p를 통해 p.FullAddress()처럼 바로 호출할 수 있습니다.
정말 편리하지 않나요?
임베딩 덕분에 가능한 마법입니다.
상속 없이 다형성 구현? 인터페이스가 해결사!
Go 언어는 전통적인 의미의 상속을 지원하지 않지만, '인터페이스(Interface)'라는 강력한 기능을 통해 다형성(Polymorphism)을 구현합니다.
다형성이 뭐냐고요?
쉽게 말해, 서로 다른 타입의 객체들이 동일한 방식으로 동작할 수 있게 하는 능력인데요.
인터페이스는 특정 타입이 반드시 구현해야 하는 메소드들의 '집합'을 정의합니다.
그리고 어떤 타입이든 이 인터페이스에 정의된 메소드들을 모두 구현하기만 하면, 그 인터페이스를 '만족시킨다(satisfy)'고 표현합니다.
자, 이것도 코드로 한번 만나볼까요?
package main
import "fmt"
// 설명을 제공하는 기능을 정의하는 인터페이스
type Describer interface {
Describe() string // Describe 메소드를 가져야 함
}
// 사람을 나타내는 구조체
type Person struct {
Name string
Age int
}
// Person 구조체가 Describer 인터페이스를 구현 (Describe 메소드 정의)
func (p Person) Describe() string {
return fmt.Sprintf("%s는 %d살입니다.", p.Name, p.Age)
}
// 제품을 나타내는 구조체
type Product struct {
Name string
Price float64
}
// Product 구조체가 Describer 인터페이스를 구현 (Describe 메소드 정의)
func (p Product) Describe() string {
return fmt.Sprintf("%s의 가격은 $%.2f입니다.", p.Name, p.Price)
}
// Describer 인터페이스를 만족하는 모든 타입을 인자로 받을 수 있는 함수
func PrintDescription(d Describer) {
fmt.Println(d.Describe())
}
func main() {
// Person 구조체 변수 생성
p := Person{Name: "데이브", Age: 22}
// Product 구조체 변수 생성
pr := Product{Name: "노트북", Price: 999.99}
// PrintDescription 함수에 Person 타입과 Product 타입 변수 모두 전달 가능
PrintDescription(p)
PrintDescription(pr)
}
이 예제에서 Person 구조체와 Product 구조체는 모두 Describe라는 메소드를 가지고 있기 때문에, Describer 인터페이스를 만족합니다.
그래서 PrintDescription 함수는 Person 타입의 변수 p와 Product 타입의 변수 pr을 모두 인자로 받을 수 있는 것이죠.
이것이 바로 Go 언어에서 인터페이스를 통해 다형성을 구현하는 방식입니다.
명시적으로 "이 클래스는 저 클래스를 상속받는다"라고 선언하지 않아도, 필요한 기능을 구현하는 것만으로도 유연하게 코드를 작성할 수 있습니다.
마무리하며: Go 언어의 유연함, 조합과 인터페이스에서 찾다!
지금까지 Go 언어가 전통적인 상속 대신 구조체 조합과 인터페이스를 통해 어떻게 유연하고 강력한 프로그래밍을 지원하는지 살펴봤습니다.
구조체를 포함시키고 인터페이스를 구현하는 방식을 통해, 우리는 유지보수하기 쉽고 이해하기 편하면서도 복잡하고 재사용 가능한 코드 구조를 만들 수 있습니다.
Go 언어의 이러한 접근 방식은 코드를 더 깔끔하고 직관적으로 만들어주며, 대규모 프로젝트에서도 빛을 발한답니다.
앞으로 Go 언어로 멋진 프로그램을 만들어나가는 데 이 글이 작은 도움이 되었으면 좋겠습니다!
'Go' 카테고리의 다른 글
| Go 언어 클로저 완전 정복: 변수를 '기억'하는 똑똑한 함수 만들기! (0) | 2025.06.03 |
|---|---|
| Go 언어에서 배열에 특정 값이 있는지 확인하는 꿀팁! (feat. 슬라이스 활용법) (1) | 2025.06.03 |
| 고(Go) JSON 인코딩의 숨은 병기, omitempty 태그 완벽 분석! (깔끔한 JSON 만들기 꿀팁) (0) | 2025.05.30 |
| 고(Go) 언어의 'goto' 문, 과연 필요악일까? 제대로 알고 사용하기 위한 모든 것! (0) | 2025.05.30 |
| 고(Go) 개발자를 위한 필수템! 데이터베이스 마이그레이션, '구스(Goose)'로 쉽고 빠르게! (핵심 기능 총정리) (0) | 2025.05.30 |