Menu

Golang 구조체 임베딩: 임베딩과 승격된 메서드

임베딩은 필드 이름 없이 한 타입을 다른 타입 안에 넣어서, 그 필드와 메서드를 바깥 타입으로 승격시킵니다. 승격의 동작 방식, 인터페이스 임베딩, 이름 충돌, 그리고 임베딩이 상속이 아닌 이유를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

예제 하나로 보는 임베딩

타입은 있고 이름은 없는 필드가 임베딩된 필드입니다. 임베딩된 타입의 필드와 메서드를 바깥 타입에서 바로 쓸 수 있게 됩니다.

출력:

Ana
Hi, I'm Ana
ana@example.com
{User:{Name:Ana Email:ana@example.com} Level:2}

임베딩된 필드의 이름은 타입 이름인 User입니다. 리터럴에서 가리킬 때(User: User{...})와 안쪽 값 자체가 필요할 때(a.User) 이 이름을 씁니다. 승격된 필드는 리터럴에서 짧은 이름으로 설정할 수 없습니다. Admin{Name: "Ana"}unknown field Name in struct literal of type Admin으로 실패합니다.

승격된 메서드는 인터페이스를 만족시킨다

승격은 임베딩된 타입의 메서드를 바깥 타입의 메서드 집합에 추가합니다. 그래서 바깥 타입은 안쪽 타입이 만족하는 모든 인터페이스를 만족합니다.

메서드 집합 규칙은 임베딩된 타입을 따릅니다. T를 임베딩하면 T의 값 리시버 메서드는 바깥 값과 바깥 포인터 모두로 승격되지만, *T의 포인터 리시버 메서드는 바깥 포인터로만 승격됩니다. *T를 임베딩하면 두 집합이 모두 양쪽으로 승격됩니다. 확실하지 않다면 포인터를 임베딩하거나 바깥 타입을 포인터로 쓰세요.

포인터 임베딩에는 대가가 있습니다. 포인터가 nil일 수 있다는 것입니다. Logger 없는 Service{}는 컴파일되지만, 그 뒤 svc.Log(...)에서 패닉이 납니다.

임베딩은 상속이 아니다

임베딩은 서브클래싱처럼 보이지만 Java나 Python과 다르게 동작하는 점이 세 가지 있습니다.

바깥 타입은 안쪽 타입이 아닙니다. AdminUser가 아닙니다. User를 받는 함수는 Admin을 받지 않습니다. a.User를 넘기세요.

가상 디스패치가 없습니다. 승격된 메서드는 임베딩된 값에서 실행되며 바깥 타입에 대해 아무것도 모릅니다. 다른 메서드를 호출하면, 바깥 타입에 같은 이름의 메서드가 있더라도 자기 타입의 버전을 호출합니다.

출력:

woof
The animal says ...

클래스 기반 언어라면 Speak는 "woof"를 출력할 것입니다. Go에서 d.Speak()d.Animal.Speak()의 줄임말이고, 그 호출의 리시버는 Animal입니다. 타입에 따라 달라지는 동작이 필요하면 인터페이스를 쓰세요. 그 동작이 필요한 코드에 Sounder를 넘기면 됩니다.

안쪽 타입은 자신이 임베딩되었다는 것을 모릅니다. super는 없습니다. 승격된 메서드를 확장하려면 바깥 타입에 같은 이름의 메서드를 정의하고 안쪽 메서드를 명시적으로 호출하세요:

func (a Admin) Greet() string {
	return a.User.Greet() + " (admin)"
}

이름 충돌과 가림

바깥 타입의 필드나 메서드는 같은 이름의 승격된 것을 가립니다. 위에서 Dog.Sound가 그랬습니다. 더 얕은 이름이 이깁니다.

같은 깊이의 임베딩된 타입 두 개가 같은 이름을 승격하면 그 이름은 모호해집니다. 모호한 이름을 쓰지 않는 한 프로그램은 여전히 컴파일됩니다:

전체 경로를 쓰거나 바깥 타입에 ID를 정의해서 충돌을 해결하세요.

인터페이스 임베딩

인터페이스는 다른 인터페이스를 임베딩할 수 있습니다. 표준 라이브러리는 이 방식으로 더 큰 인터페이스를 만듭니다:

type ReadWriter interface {
	Reader
	Writer
}

구조체도 인터페이스를 임베딩할 수 있습니다. 그러면 구조체는 그 필드에 저장한 값을 통해 인터페이스를 만족하고, 관심 있는 메서드만 덮어쓰면 됩니다. 코드가 거의 없는 데코레이터 패턴입니다:

명시적인 c.Reader.Read(p)가 필요합니다. Read 안에서 c.Read(p)라고 쓰면 자기 자신을 끝없이 호출합니다.

테스트용 가짜 객체에 인터페이스를 임베딩하는 것은 흔한 지름길입니다. 큰 인터페이스를 임베딩하고, 테스트에 필요한 메서드 하나만 구현하고, 나머지는 그대로 둡니다. 구현하지 않은 메서드를 호출하면 nil 포인터 역참조로 패닉이 나는데, 테스트에서는 대개 바라는 동작입니다.

흔한 실제 용도: sync.Mutex 임베딩

type Stats struct {
	sync.Mutex
	hits map[string]int
}

func (s *Stats) Hit(page string) {
	s.Lock()
	defer s.Unlock()
	s.hits[page]++
}

읽기에는 좋지만 LockUnlockStats API의 일부로 공개되므로, 어떤 호출자든 여러분의 구조체를 잠글 수 있습니다. 패키지 밖에서 쓰이는 타입이라면 이름 있는 필드(mu sync.Mutex)로 잠금을 비공개로 유지하세요. 이 트레이드오프는 모든 임베딩에 해당합니다. 임베딩된 타입이 공개하는 모든 것이 여러분 타입의 공개 표면이 됩니다.

흔한 실수

  • 가상 디스패치를 기대함. 승격된 메서드는 바깥 타입이 덮어쓴 메서드를 절대 호출하지 않습니다.
  • 리터럴에서 승격된 필드를 설정함. 임베딩된 타입의 이름을 쓰세요: Admin{User: User{Name: "Ana"}}.
  • nil인 임베딩 포인터. 임베딩된 *T나 인터페이스는 메서드를 호출하기 전에 설정해야 합니다.
  • 의도치 않은 API 표면. 임베딩하면 안쪽 타입의 공개 메서드가 모두 여러분의 타입에서 공개됩니다.

자주 묻는 질문

Go에서 구조체 임베딩이란 무엇인가요?

이름 없이 타입만으로 필드를 선언하는 것입니다: type Admin struct { User; Level int }. 임베딩된 User의 필드와 메서드가 승격되므로 Admin에서 바로 a.Namea.Greet()을 쓸 수 있습니다. 임베딩된 값은 여전히 평범한 필드이며 a.User로 접근할 수 있습니다.

Go에 상속이 있나요?

없습니다. Go에는 클래스도, 서브타입 상속도 없습니다. 임베딩은 컴포지션을 통한 코드 재사용을 제공합니다. 바깥 타입이 안쪽 타입의 메서드를 얻지만 AdminUser인 것은 아닙니다. User가 필요한 곳에 Admin을 넘길 수 없고, 승격된 메서드는 바깥 타입의 메서드를 다시 호출할 수 없습니다. Go의 다형성은 인터페이스에서 나옵니다.

Go에서 임베딩된 구조체는 어떻게 초기화하나요?

복합 리터럴에서 임베딩된 필드를 타입 이름으로 지정합니다: Admin{User: User{Name: "Ana"}, Level: 2}. 리터럴에서 승격된 필드를 직접 설정할 수는 없습니다. Admin{Name: "Ana"}unknown field Name in struct literal of type Admin으로 실패합니다.

Go에서 구조체에 인터페이스를 임베딩할 수 있나요?

네. 그러면 구조체는 임베딩된 값을 통해 인터페이스를 만족하고, 개별 메서드를 덮어쓸 수 있습니다. 데코레이터와 테스트용 가짜 객체에 흔히 씁니다. 임베딩된 인터페이스 필드가 nil이면, 덮어쓰지 않은 메서드를 호출할 때 nil 포인터 역참조로 패닉이 납니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기