[Go] Embedding

김무연·2025년 2월 7일

GoLang의 문법 뿌시기

목록 보기
11/14

이미 정의한 무언가(타입)을 재활용하여 새로운 무언가(타입)을 만드는 것을 Embedding 이라고 한다. 이러한 방법으로는 대표적으로 상속컴포지션 이 있다.

  • 상속 : 하위 클래스가 상위 클래스의 특성을 재정의, (IS-A) 관계
  • 컴포지션 : 하위 클래스가 상위 클래스를 포함, (HAS-A) 관계

많은 언어가 이미 정의된 타입을 참고해 새로운 타입을 정의할 떄, 상속을 주로 사용하였다. 하지만 이러한 상속은 몇 가지 문제점을 가지고 있다.

  1. Open Close Principle (OCP) 을 위반할 수 있다.
  • OCP는 소프트웨어 구성 요소(컴포넌트, 클래스, 모듈, 함수) 확장에 대해서는 개방(Open) 되어야 하지만 변경에 대해서는 폐쇄(Close) 되어야 한다는 의미, 즉 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다.

  • 하지만 상속은 오버라이딩을 통해서 부모 클래스의 함수를 수정할 수 있기 때문에 OCP를 위반한다
  1. 캡슐화를 위반할 수 있다.
  • 자식 클래스가 부모 클래스를 수정하는 경우, 부모 클래스의 내부 동작에 접근해 동작을 수정하는 경우가 생긴다. 이는 캡슐화를 위반한다.

컴포지션은 원래 클래스를 수정할 수 없기 때문에 개방폐쇄 원칙을 강제한다. 또한 원래 클래스가 캡슐화를 잘 지켜 만들었다면, 이를 포함하는 새로운 클래스는 원래 클래스의 내부동작에 간섭하지 못하므로 캡슐화 또한 지켜진다.

이러한 문제점에 따라, Go는 상속기능을 지원하지 않고, 오로지 컴포지션(Go Embedding) 기능만을 지원하도록 설계되었다.

type Base struct {
	Name string
	Age int
}

type EmbedBase struct {
	Base
	Price int
}

타입 임베딩 vs 포인터 임베딩

임베딩의 방식에는 두 가지 방식이 존재한다. 하나는 타입을 임베딩하는 것이고, 다른 하나는 타입의 포인터를 임베딩하는 방식이다.

  1. 타입 임베딩

위 코드에서 EmbedBase 구조체는 Base의 필드들을 모두 가지며, Price 라는 새로운 필드를 가지는 새로운 타입니다.

  1. 포인터 임베딩
type Base struct {
	Name string
	Age  int
}

type EmbedPointerBase struct {
	*Base
	Price int
}

EmbedPointerBase 의 첫 번째 필드를 보면 Base의 포인터를 필드로 갖는 새로운 타입이다.

이 두 방식은 객체 생성 및 전달에서 차이가 있으며, 객체 내부 변수에 어떻게 접근할 것인가에 따라 사용처가 달라질 것이다. 이전 포스팅에서도 꾸준히 나왔었지만, pass by value 와 pass by reference 의 차이처럼 말이다.

Base의 메서드가 Base의 값을 제어하고, 이때 이 Base를 임베딩한 EmbedBase 객체를 값으로 넘기는 특수한 함수를 가정하면, 함수 내부에서 Base를 조작하는 것은 외부에 영향을 주지 못 한다.

package embedding

import "fmt"

type Base struct {
	Name string
	Age  int
}

type EmbedBase struct {
	Base
	Price int
}

type EmbedPointerBase struct {
	*Base
	Price int
}

func (b Base) printBase() {
	fmt.Println(b.Name, b.Age)
}

func changeBase(baseinfo *EmbedBase) {
	baseinfo.Name = "changed name"
	baseinfo.Age = 3
}

func Embedding() {
	baseinfo := Base{"name", 2}
	embedbaseinfo := EmbedBase{baseinfo, 30000}
	embedbaseinfo.printBase()

	changeBase(&embedbaseinfo)
	baseinfo.printBase()
}

////
name 2
name 2

위 코드에서 EmbedBase에 Base struct가 담긴 후, 해당 embedbaseinfo의 주소값을 보내서 값을 수정을 했지만, 원본인 Base에는 변화가 없는 것을 알 수 있다. 하지만 여기서 포인터를 임베딩한 struct를 이용하면 원본에 변화를 줄 수 있을 것이다.

package embedding

import "fmt"

type Base struct {
	Name string
	Age  int
}

type EmbedBase struct {
	Base
	Price int
}

type EmbedPointerBase struct {
	*Base
	Price int
}

func (b Base) printBase() {
	fmt.Println(b.Name, b.Age)
}

func changeBase(baseinfo EmbedPointerBase) {
	baseinfo.Name = "changed name"
	baseinfo.Age = 3
}

func Embedding() {
	baseinfo := Base{"name", 2}
	embedbaseinfo := EmbedPointerBase{&baseinfo, 30000}
	embedbaseinfo.printBase()

	changeBase(embedbaseinfo)
	baseinfo.printBase()
}
////
name 2
changed name 3
profile
Notion에 정리된 공부한 글을 옮겨오는 중입니다... (진행중)

0개의 댓글