Go의 메서드 승격(method promotion)은 구조체에 다른 타입을 embedded field로 포함했을 때, embedded field의 필드나 메서드를 바깥 구조체의 selector로 직접 접근할 수 있게 되는 기능입니다.
type Logger struct {
Prefix string
}
func (l Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.Prefix, message)
}
type OrderService struct {
Logger
DBName string
}OrderService에는 필드 이름을 따로 붙이지 않고 Logger 타입만 선언했습니다.
type OrderService struct {
Logger
DBName string
}이와 같이 타입 이름만 필드로 선언한 것을 embedding이라고 합니다. Go 언어 명세에서는 이를 embedded field라고 부릅니다.
Logger의 Log 메서드는 다음처럼 원래 경로로 호출할 수 있습니다.
service.Logger.Log("주문을 처리합니다.")메서드 승격이 적용되면 바깥 구조체에서 직접 호출할 수도 있습니다.
service.Log("주문을 처리합니다.")service.Log(...)가 유효한 selector가 되어 embedded field의 Log 메서드를 가리키는 것을 메서드가 승격되었다고 표현합니다.
메서드가
OrderService에 복사되거나 새로운 메서드가 자동 생성되는 것은 아닙니다.service.Log()라는 축약된 selector로service.Logger.Log()를 호출할 수 있게 되는 것입니다.
embedding과 메서드 승격
전체 예제를 보겠습니다.
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.Prefix, message)
}
type OrderService struct {
Logger
DBName string
}
func main() {
service := OrderService{
Logger: Logger{
Prefix: "ORDER-SERVICE",
},
DBName: "main_database",
}
service.Logger.Log("embedded field를 통해 호출")
service.Log("승격된 메서드로 호출")
}출력:
[ORDER-SERVICE] embedded field를 통해 호출
[ORDER-SERVICE] 승격된 메서드로 호출두 호출은 같은 Logger.Log 메서드를 실행합니다.
service.Logger.Log("메시지")
service.Log("메시지")관계는 다음과 같습니다.
OrderService
├── Logger
│ ├── Prefix
│ └── Log()
└── DBNameLog 메서드가 승격되면 다음 selector를 사용할 수 있습니다.
service.Logger.Log()
service.Log()임베딩은 상속이 아닙니다. OrderService가 Logger의 하위 타입이 되는 것이 아니라 Logger 값을 필드로 포함하는 별도의 구조체 타입입니다.
OrderService has a Logger → 맞음
OrderService is a Logger → 아님따라서 Logger를 받는 함수에 OrderService 자체를 전달할 수는 없습니다.
func PrintLogger(logger Logger) {
logger.Log("호출")
}service := OrderService{
Logger: Logger{Prefix: "ORDER"},
}
// PrintLogger(service) // 컴파일 오류
PrintLogger(service.Logger) // 가능필드 승격과 이름 충돌
메서드뿐 아니라 embedded field가 가진 필드도 승격될 수 있습니다.
type Logger struct {
Prefix string
}
type OrderService struct {
Logger
DBName string
}원래 경로로 접근할 수 있습니다.
fmt.Println(service.Logger.Prefix)승격된 필드 selector로도 접근할 수 있습니다.
fmt.Println(service.Prefix)service.Prefix는 service.Logger.Prefix를 가리킵니다.
바깥 타입에 같은 이름이 있는 경우
바깥 구조체에 같은 이름의 필드가 있으면 더 얕은 깊이의 필드가 선택됩니다.
type Logger struct {
Prefix string
}
type OrderService struct {
Logger
Prefix string
}service := OrderService{
Logger: Logger{
Prefix: "LOGGER",
},
Prefix: "SERVICE",
}
fmt.Println(service.Prefix) // SERVICE
fmt.Println(service.Logger.Prefix) // LOGGER메서드도 같은 원리가 적용됩니다.
type Logger struct{}
func (Logger) Log(message string) {
fmt.Println("[LOGGER]", message)
}
type OrderService struct {
Logger
}
func (OrderService) Log(message string) {
fmt.Println("[SERVICE]", message)
}service := OrderService{}
service.Log("호출") // OrderService.Log
service.Logger.Log("호출") // Logger.Log이는 상속 언어의 메서드 overriding과 동일한 개념이 아닙니다. selector를 찾을 때 더 얕은 깊이에 선언된 메서드가 선택되는 것입니다.
embedded field의 메서드는 사라지지 않으므로 전체 경로를 사용해 계속 호출할 수 있습니다.
같은 깊이에서 이름이 충돌하는 경우
두 embedded field가 같은 이름의 필드나 메서드를 제공하면 축약 selector가 모호해질 수 있습니다.
type FileLogger struct{}
func (FileLogger) Log(message string) {
fmt.Println("[FILE]", message)
}
type ConsoleLogger struct{}
func (ConsoleLogger) Log(message string) {
fmt.Println("[CONSOLE]", message)
}
type Service struct {
FileLogger
ConsoleLogger
}다음 selector는 어느 Log를 의미하는지 결정할 수 없으므로 사용할 수 없습니다.
service := Service{}
// service.Log("호출") // 컴파일 오류: ambiguous selector각 embedded field를 명시하면 호출할 수 있습니다.
service.FileLogger.Log("파일 로그")
service.ConsoleLogger.Log("콘솔 로그")구조체 선언 자체가 반드시 오류가 되는 것은 아닙니다. 모호한 service.Log selector를 사용하려 할 때 컴파일 오류가 발생합니다.
어떤 타입을 embedding할 수 있는가
구조체의 embedded field에는 일반적으로 타입 이름 T 또는 허용되는 경우 타입 이름을 가리키는 포인터 *T를 사용할 수 있습니다.
구조체 타입 embedding
가장 일반적인 형태입니다.
type Logger struct{}
func (Logger) Log(message string) {
fmt.Println(message)
}
type Service struct {
Logger
}service := Service{}
service.Log("호출")구조체 포인터 embedding
포인터 타입을 embedded field로 둘 수도 있습니다.
type Logger struct {
Prefix string
}
func (l *Logger) Log(message string) {
fmt.Printf("[%s] %s\n", l.Prefix, message)
}
type Service struct {
*Logger
}초기화:
service := Service{
Logger: &Logger{
Prefix: "SERVICE",
},
}
service.Log("호출")embedded pointer가 nil이면 메서드 구현에 따라 panic이 발생할 수 있습니다.
service := Service{}
// service.Log("호출") // Logger가 nil이므로 메서드 내부 접근 시 panic 가능따라서 포인터를 임베딩할 때는 사용할 수 있는 값으로 초기화되었는지 확인해야 합니다.
이름 있는 기본 타입 embedding
기본 타입을 기반으로 정의한 이름 있는 타입도 embedding할 수 있습니다.
type Text string
func (t Text) Length() int {
return len(t)
}
type Message struct {
Text
}message := Message{
Text: "hello",
}
fmt.Println(message.Text)
fmt.Println(message.Length())Text.Length 메서드가 Message로 승격됩니다.
미리 선언된 타입 embedding
int, string과 같은 미리 선언된 타입 이름도 embedded field로 사용할 수 있습니다.
type Counter struct {
int
}counter := Counter{
int: 10,
}
fmt.Println(counter.int)그러나 int 자체에는 메서드가 정의되어 있지 않으므로 승격될 메서드는 없습니다.
메서드 승격이 발생하려면 embedded field의 타입에 승격 가능한 메서드가 있어야 합니다.
인터페이스 embedding
구조체에 인터페이스 타입을 embedding할 수도 있습니다.
type Reader interface {
Read() string
}
type FileReader struct {
Content string
}
func (f FileReader) Read() string {
return f.Content
}
type Document struct {
Reader
}document := Document{
Reader: FileReader{
Content: "파일 내용",
},
}
fmt.Println(document.Read())Reader 인터페이스의 Read 메서드가 Document에서 호출 가능한 메서드로 승격됩니다.
embedded interface field에 실제 값이 들어 있지 않으면 호출할 수 없습니다.
var document Document
// document.Read() // nil 인터페이스를 통한 호출로 panic 발생구조체 안에 인터페이스를 embedding하면 해당 인터페이스의 메서드가 바깥 타입의 method set에도 영향을 줄 수 있습니다. 그러나 실제 호출은 embedded interface field에 저장된 구체 값이 처리합니다.
허용되지 않는 형태
포인터를 가리키는 포인터를 embedded field로 직접 선언할 수는 없습니다.
type Logger struct{}
type Service struct {
// **Logger // 허용되지 않음
}포인터 타입에 별도의 이름을 붙였더라도 그 이름 있는 타입 자체가 포인터 타입이면 embedded field로 사용할 수 없습니다.
type LoggerPointer *Logger
type Service struct {
// LoggerPointer // 허용되지 않음
}인터페이스를 가리키는 포인터도 embedded field로 사용할 수 없습니다.
type Reader interface {
Read() string
}
type Service struct {
// *Reader // 허용되지 않음
}인터페이스 값 자체가 이미 동적 타입과 동적 값을 담을 수 있으므로 일반적으로 인터페이스 포인터를 사용할 이유도 없습니다.
값 embedding과 포인터 embedding의 method set
메서드 승격에서 가장 중요한 부분은 메서드를 직접 호출할 수 있는지와 바깥 타입의 method set에 어떤 메서드가 포함되는지를 구분하는 것입니다.
다음 타입을 사용하겠습니다.
type Logger struct{}
func (Logger) Info(message string) {
fmt.Println("[INFO]", message)
}
func (*Logger) Error(message string) {
fmt.Println("[ERROR]", message)
}Info는 value receiver 메서드이고 Error는 pointer receiver 메서드입니다.
값 타입 Logger를 embedding한 경우
type Service struct {
Logger
}Go 명세의 method set 규칙은 다음과 같습니다.
Service의 method set
→ Logger의 value receiver 메서드 포함
*Service의 method set
→ Logger의 value receiver와 pointer receiver 메서드 포함따라서 method set은 다음과 같습니다.
| 타입 | 승격되어 포함되는 메서드 |
|---|---|
Service |
Info |
*Service |
Info, Error |
그런데 주소를 구할 수 있는 Service 변수에서는 Error도 직접 호출할 수 있습니다.
service := Service{}
service.Info("정보")
service.Error("오류")service.Error(...)는 Service 값의 method set에 Error가 들어 있어서 가능한 것이 아닙니다. service가 주소를 구할 수 있는 변수이므로 Go가 (&service).Error(...) 형태의 호출을 허용하는 것입니다.
이 차이는 인터페이스 대입에서 드러납니다.
type ErrorLogger interface {
Error(string)
}service := Service{}
var logger ErrorLogger = &service // 가능다음 대입은 불가능합니다.
var logger ErrorLogger = service // 컴파일 오류Service의 method set에는 pointer receiver에서 승격된 Error가 없기 때문입니다.
값 타입을 embedding했을 때 pointer receiver 메서드는 바깥 값 타입의 method set으로 승격되는 것이 아닙니다. 바깥 포인터 타입의 method set에 포함되며, 주소를 구할 수 있는 값에서는 메서드 호출 문법의 자동 주소 변환으로 호출할 수 있습니다.
포인터 타입 *Logger를 embedding한 경우
type Service struct {
*Logger
}이 경우 method set은 다음과 같습니다.
Service의 method set
→ Logger의 value receiver와 pointer receiver 메서드 포함
*Service의 method set
→ Logger의 value receiver와 pointer receiver 메서드 포함| 타입 | 승격되어 포함되는 메서드 |
|---|---|
Service |
Info, Error |
*Service |
Info, Error |
따라서 값과 포인터 모두 ErrorLogger 인터페이스를 구현할 수 있습니다.
service := Service{
Logger: &Logger{},
}
var first ErrorLogger = service
var second ErrorLogger = &service
first.Error("첫 번째")
second.Error("두 번째")다만 *Logger embedded field가 nil이면 호출 시 문제가 생길 수 있으므로 초기화가 필요합니다.
규칙 정리
| embedding 형태 | 바깥 값 타입 S의 method set |
바깥 포인터 타입 *S의 method set |
|---|---|---|
T |
receiver가 T인 승격 메서드 |
receiver가 T, *T인 승격 메서드 |
*T |
receiver가 T, *T인 승격 메서드 |
receiver가 T, *T인 승격 메서드 |
포인터 embedding이 항상 더 좋다는 뜻은 아닙니다.
포인터 embedding을 선택하면 다음 특성이 생깁니다.
- embedded value를 여러 바깥 구조체가 공유할 수 있습니다.
- embedded field가 nil일 수 있습니다.
- 값 타입의 method set에도 pointer receiver 메서드가 포함됩니다.
- 생성과 초기화 정책을 더 명확히 관리해야 합니다.
값 embedding을 선택하면 embedded value가 바깥 구조체 값의 일부로 포함됩니다. 어떤 방식을 사용할지는 공유 여부, nil 허용 여부, 복사 의미와 필요한 method set을 기준으로 결정해야 합니다.
전체 실행 예제
다음 예제는 필드 승격, 메서드 승격, 이름 충돌과 값·포인터 embedding의 method set 차이를 확인합니다.
package main
import "fmt"
type Logger struct {
Prefix string
}
func (l Logger) Info(message string) {
fmt.Printf("[%s][INFO] %s\n", l.Prefix, message)
}
func (l *Logger) Error(message string) {
fmt.Printf("[%s][ERROR] %s\n", l.Prefix, message)
}
type ErrorLogger interface {
Error(string)
}
type ValueEmbeddedService struct {
Logger
Name string
}
type PointerEmbeddedService struct {
*Logger
Name string
}
type CustomService struct {
Logger
}
func (CustomService) Info(message string) {
fmt.Println("[CUSTOM][INFO]", message)
}
func main() {
fmt.Println("1. 값 타입 embedding")
valueService := ValueEmbeddedService{
Logger: Logger{
Prefix: "VALUE",
},
Name: "value-service",
}
fmt.Println("승격된 필드:", valueService.Prefix)
valueService.Info("value receiver 메서드")
valueService.Error("주소를 구할 수 있어 호출 가능한 pointer receiver 메서드")
var valueErrorLogger ErrorLogger = &valueService
valueErrorLogger.Error("인터페이스에는 *ValueEmbeddedService 대입")
// 다음 대입은 컴파일되지 않습니다.
// var invalid ErrorLogger = valueService
fmt.Println()
fmt.Println("2. 포인터 타입 embedding")
pointerService := PointerEmbeddedService{
Logger: &Logger{
Prefix: "POINTER",
},
Name: "pointer-service",
}
pointerService.Info("value receiver 메서드")
pointerService.Error("pointer receiver 메서드")
var first ErrorLogger = pointerService
var second ErrorLogger = &pointerService
first.Error("값을 인터페이스에 대입")
second.Error("포인터를 인터페이스에 대입")
fmt.Println()
fmt.Println("3. 바깥 메서드와 이름 충돌")
customService := CustomService{
Logger: Logger{
Prefix: "LOGGER",
},
}
customService.Info("바깥 타입의 메서드")
customService.Logger.Info("embedded field의 메서드")
}실행:
go run .출력 예:
1. 값 타입 embedding
승격된 필드: VALUE
[VALUE][INFO] value receiver 메서드
[VALUE][ERROR] 주소를 구할 수 있어 호출 가능한 pointer receiver 메서드
[VALUE][ERROR] 인터페이스에는 *ValueEmbeddedService 대입
2. 포인터 타입 embedding
[POINTER][INFO] value receiver 메서드
[POINTER][ERROR] pointer receiver 메서드
[POINTER][ERROR] 값을 인터페이스에 대입
[POINTER][ERROR] 포인터를 인터페이스에 대입
3. 바깥 메서드와 이름 충돌
[CUSTOM][INFO] 바깥 타입의 메서드
[LOGGER][INFO] embedded field의 메서드정리
Go의 메서드 승격은 embedded field의 메서드를 바깥 구조체의 selector로 직접 호출할 수 있게 하는 기능입니다.
service.Logger.Log("호출")
service.Log("호출")두 호출은 embedded field에 정의된 같은 메서드를 실행합니다.
메서드 승격은 상속이 아닙니다.
바깥 구조체가 embedded field를 포함함
→ composition
바깥 구조체가 embedded 타입의 하위 타입이 됨
→ 아님필드와 메서드 이름이 충돌할 때는 가장 얕은 깊이의 selector가 선택됩니다. 같은 깊이에서 둘 이상의 후보가 있으면 축약 selector는 모호하므로 각 embedded field의 경로를 명시해야 합니다.
값 타입 T와 포인터 타입 *T의 embedding은 바깥 타입의 method set에 다른 결과를 만듭니다.
T를 embedding
→ S에는 T receiver 메서드
→ *S에는 T와 *T receiver 메서드
*T를 embedding
→ S와 *S 모두 T와 *T receiver 메서드주소를 구할 수 있는 값에서 pointer receiver 메서드를 직접 호출할 수 있다는 사실과, 그 메서드가 값 타입의 method set에 포함된다는 것은 다른 개념입니다.
포인터 embedding이 항상 더 안전한 방식은 아닙니다. nil 가능성, 값 공유 여부, 복사 의미와 인터페이스 구현에 필요한 method set을 고려해 값 또는 포인터 embedding을 선택해야 합니다.
참고 자료
- Go 언어 명세 - Struct types: https://go.dev/ref/spec#Struct_types
- Go 언어 명세 - Selectors: https://go.dev/ref/spec#Selectors
- Go 언어 명세 - Method sets: https://go.dev/ref/spec#Method_sets
- Effective Go - Embedding: https://go.dev/doc/effective_go#embedding
'프로그래밍 > Go' 카테고리의 다른 글
| Go select, timeout, close, context (0) | 2026.07.20 |
|---|---|
| Goroutine과 channel (1) | 2026.07.18 |
| Go 인터페이스의 암시적 구현과 Java 인터페이스와의 차이 (0) | 2026.07.15 |
| Go struct와 receiver (0) | 2026.07.15 |
| Go struct, method, interface, enum 패턴과 embedding (0) | 2026.07.15 |
댓글