Go:Embedding
About
Go에서의 임베딩은 한 구조체 안에 다른 구조체나 인터페이스를 필드 이름 없이, 타입 이름만으로 포함시키는 것을 말합니다.
먼저, 우리가 일반적으로 아는 '조합 (Composition)' 방식부터 보겠습니다.
Composition
type Engine struct {
Horsepower int
}
func (e *Engine) Start() {
fmt.Println("엔진 시동!")
}
type Car struct {
engineField Engine // 'engineField'라는 필드 이름을 명시
}
func main() {
myCar := Car{}
myCar.engineField.Start() // 필드 이름을 통해 메서드 호출
}
Embedding
type Engine struct {
Horsepower int
}
func (e *Engine) Start() {
fmt.Println("엔진 시동!")
}
type Car struct {
Engine // 필드 이름 없이 타입 이름만 포함
}
func main() {
myCar := Car{}
myCar.Start() // Engine의 메서드를 Car가 직접 가진 것처럼 호출!
}
메서드와 필드의 승격 (Promotion)
임베딩된 타입의 필드와 메서드는 바깥쪽 구조체의 필드와 메서드인 것처럼 직접 접근할 수 있어 코드 재사용성이 극대화됩니다.
package main
import "fmt"
type Person struct {
Name string
}
func (p Person) Greet() {
fmt.Printf("안녕하세요, 제 이름은 %s입니다.\n", p.Name)
}
// Person 구조체를 임베딩하는 Employee 구조체
type Employee struct {
Person // Person을 임베딩
EmployeeID string
}
func main() {
emp := Employee{
Person: Person{Name: "Royce"},
EmployeeID: "KE-1234",
}
// 1. 필드 승격: emp.Person.Name 대신 emp.Name으로 바로 접근
fmt.Println(emp.Name) // 출력: Royce
// 2. 메서드 승격: emp.Person.Greet() 대신 emp.Greet()으로 바로 호출
emp.Greet() // 출력: 안녕하세요, 제 이름은 Royce입니다.
}
오버라이딩(Overriding)? 아니, 메서드 섀도잉(Method Shadowing)!
만약 바깥쪽 구조체와 임베딩된 구조체에 같은 이름의 메서드가 있다면 어떻게 될까요? 이때는 바깥쪽 구조체의 메서드가 우선권을 갖습니다. 즉, 바깥쪽 메서드가 안쪽 메서드를 '가리는(shadows)' 효과가 나타납니다.
type Employee struct {
Person
EmployeeID string
}
// Employee에 Greet 메서드를 새로 정의
func (e Employee) Greet() {
fmt.Printf("카카오엔터프라이즈 직원, ")
// 만약 임베딩된 타입의 메서드를 호출하고 싶다면, 명시적으로 접근해야 합니다.
e.Person.Greet()
}
func main() {
emp := Employee{ /* ... */ }
emp.Greet()
// 출력: 카카오엔터프라이즈 직원, 안녕하세요, 제 이름은 Royce입니다.
}
emp.Greet()를 호출하면 Employee에 직접 정의된 Greet가 실행됩니다. 이것이 Go에서 메서드 오버라이딩과 가장 유사하게 동작하는 방식입니다.
가장 중요한 차이점: 임베딩은 상속이 아니다
임베딩은 편리하지만 상속이 아닙니다. 둘의 가장 큰 차이는 is-a 관계와 has-a 관계에 있습니다.
- 상속 (is-a 관계): 고양이는 동물이다. 자식 클래스는 부모 클래스 타입으로 취급될 수 있습니다.
- 임베딩 (has-a 관계): Employee는 Person을 가지고 있는(has-a) 것입니다. Go 타입 시스템 관점에서 Employee는 Person이 아닙니다.
이를 증명하는 코드는 다음과 같습니다.
func SayHelloToPerson(p Person) {
p.Greet()
}
func main() {
emp := Employee{Person: {Name: "Royce"}}
// SayHelloToPerson(emp) // 컴파일 에러!
// 에러 메시지: cannot use emp (variable of type Employee) as Person value in argument to SayHelloToPerson
// Employee는 Person이 아니므로, Person을 받는 함수에 직접 전달할 수 없습니다.
// 명시적으로 Person 필드를 전달해야만 합니다.
SayHelloToPerson(emp.Person) // OK!
}
컴파일러는 Employee가 Person 타입이 아니라고 명확히 알려줍니다. 이것이 Go가 '상속'의 복잡성을 피하고 '조합'을 선호하는 방식을 보여주는 결정적인 증거입니다.