Go/Go Basic

Go Interface, embedded

guiwoo 2023. 2. 28. 17:26

최근 디자인 패턴을 공부하면서 Interface, Type에 대해 부족한 부분을 발견하고 이번 기회에 쉬는 날 정리해 보고자. 글을 작성한다.

GO TOUR 에서 작성된 글에 의하면? 

인터페이스 타입 은 특정 함수를 정의 하는 용도로 사용한다. Java에 익숙한 사람들이라면 아래 예제는 당연하고 이해가 간다.

type Sender interface {
	send()
}

type Bank struct {
	account Account
}

func (b *Bank) send() {
	fmt.Printf("%v 에서 돈을 보냅니다 .", b.account.ID)
}

type Account struct {
	ID      string
	Balance uint
}

func main() {
	var a Sender = &Bank{Account{"귀우 의 계좌", 123}}
	a.send()
}

Sender 인터페이스 를 선언 (er을 이용해 Interface를 정의한다 "GO effecitve") 

뱅크의 타입을 선언 했지만? Sender의 타입으로 받아서 샌더가 가지고 있는 함수를 사용할 수 있다.

 

당연하다 그렇다면 역으로도 가능한가 ? 저 특정 인터페이스에서 원하는 타입을 뽑아야 내하는 경우가 생긴다면 어떻게 할 것인가? 

가능하다.  어떻게 ?

bank := &Bank{Account{"귀우 의 계좌", 123}}
var a Sender = bank
a.send()

if x, ok := a.(*Bank); ok {
   x.send()
}

Interface로 선언된 타입은 최초 구현은 Bank의 포인터 타입이다.

추출하고자 하는 정확한 타입에 대해 위와 같이 추출해서 사용한다.

 

좋다 인터페이스 와 구현하는 구조체 간의 쌍방향 관계가 된다 는 사실을 알았으니 이제 타입 끼리 해보자.

 

동일한 인터페이스를 구현하는 2개 의 타입 간의 크로스 캐스팅이 가능한가?

type Email struct {
	sender   string
	receiver string
	address  string
}

func (e *Email) send() {
	fmt.Printf("%v 에서 %v 읭 %v 로 보냅니다. ", e.sender, e.receiver, e.address)
}

func main() {
	var a Sender = email
	if v, ok := a.(*Email); ok {
		v.send()
	}
	if v, ok := a.(*Bank); ok {
		v.send()
	}
 }

 

 

이 경우 콘솔에서는 메일에 해당하는 샌더를 실행시켜 준다. 즉 2번째 if 문이 false로 타입추출에 실패했다.

그래서 이게 뭐? 당연한 거 아닌가?

 

아래 예제를 보자.

type ExpressEmail struct {
	*Email
	price int
}

func main() {
	//bank := &Bank{Account{"귀우 의 계좌", 123}}
	email := &ExpressEmail{
		&Email{"a", "b", "13472 Mortgatan6 Saltjobaden"},
		10000000,
	}
	var a Sender = email
	fmt.Println(reflect.TypeOf(a))
	if v, ok := a.(*Email); ok {
		v.send()
	}
	if v, ok := a.(*ExpressEmail); ok {
		v.superFast()
		v.send()
	}
}

ExpressEmail로 인터페이스를 받아서 Email을 뽑으려고 하면 실패한다.  왜? ExpresEmail 구현체 안의 Email 도 하나의 고유한 타입이기 때문에 위와 같이 타입 추출이 불가능하다. (반면 자바는 가능하다.)

 

여기서 언어의 차이가 드러난다. 객체 지향과, 그렇지 않은 그래서 클래스의 상속에 따른 구조와 임베디드 타입에 의한 새로운 타입의 차이 가 있기 때문에 위와 같은 동일한 로직에서 서로 다른 결과를 얻게 된다.

 

반면 인터페이스 안에 인터페이스로 정의된다면 그건 타입캐스팅이 될까? 

type Sender interface {
	send()
}

type Receiver interface {
	receive()
}

type SendReceive interface {
	Sender
	Receiver
}

type CommonSendReceive struct{}

func (c *CommonSendReceive) send() {
	fmt.Println("Send Common Message")
}
func (c *CommonSendReceive) receive() {
	fmt.Println("Receive Common Message")
}

func main() {
	var sr SendReceive = &CommonSendReceive{}

	if v, ok := sr.(Sender); ok {
		v.send()
	}

	if v, ok := sr.(Receiver); ok {
		v.receive()
	}
}

놀랍게도 가능하다. 인터페이스 안에 임베디드 타입으로 구성된 인터페이스에 대해서는 타입 추출이 가능해진다.

 

왜 그럴까?

 

타입 추출은 컴파일러에게 인터페이스를 구현하는 것으로부터 내가 원하는 것처럼 뽑아달라고 부탁하는 것이다. 

이렇게 되면 인터페이스 값에 기반하지 않은 타입이라면 에러가 발생하게 된다.

그러나 인터페이스 유니온 즉 인터페이스끼리 결합된 인터페이스에서는 분리가 가능해진다. 

인터페이스의 결합을 코드로 풀면? 그냥 두 개의 메서드 합친 결과물이다. 그러니 당연히 필요한 부분의 분리가 가능해진다.

 

왜? 정확히는 컴파일러에게 내가 원하는 메서드를 어떤 걸 사용할 건데 그 타입으로 뽑아줘라고 부탁을 하는 것이기 때문이다.

 

아까 예제 ExpressEmail에서 email을 뽑아보자.

type Sender interface {
	send()
}

type Email interface {
	sendEmail()
}
type email struct{}

func (e *email) sendEmail() {
	fmt.Println("고유 이메일")
}

type ExpressEmail struct {
	Email
	price int
}

func (e *ExpressEmail) send() {
	fmt.Println("Send Express Email")
}

아까 와의 차이가 무엇인가? 인터페이스 타입을 인자로 가지냐 아니면 특정 타입이냐의 차이가 되고 그 결과는 타입 추출 의 가능 여부를 판별한다.

func main() {
	var a Sender = &ExpressEmail{
		&email{},
		500,
	}

	if v, ok := a.(*ExpressEmail); ok {
		v.send()
	}
	if v, ok := a.(Email); ok {
		v.sendEmail()
	}
}

위와 같은 코드는 전부 실행이 된다. 인터페이스의 강력함이 아닐수가 없다... 

심지어 인터페이스가 아닌 구조체를 넣게 되더라도 그게 특정 인터페이스를 구현하고 있다면 인터페이스 의 타입추출이 가능해진다.

func main() {
	var a Sender = &ExpressEmail{
		email{},
		500,
	}

	if v, ok := a.(*ExpressEmail); ok {
		v.send()
	}
	if v, ok := a.(Email); ok {
		v.sendEmail()
		fmt.Println(reflect.TypeOf(v))
		if e, ok := v.(*email); ok {
			fmt.Println(reflect.TypeOf(e))
		}
	}

}

이 코드에서 보면  email이라는 구조체는 Email 인터페이스를 구현하고 있고 우리는 그 안에 email 있다는 사실을 알고 역으로 풀고자 한다.

그러나 마지막 if 문에서 프린트 라인이 찍히지 않는다. 왜?

reflect를 이용해 타입 추출을 하더라도 저 타입은 아직도 ExpressEmail이다. 즉 cpu는 이렇게 알고 있다는 소리이다. 

아무리 우리가 밖에서 Email로 추론하고 값을 달라고 하더라도 cpu에서 지정된 오리지널 값에서 잘라서 준다는 의미이다.

 

아무래도 고에서 지원하는 상속과 embedded 타입 간의 간극을 잘 파악하여 사용하지 않는다면 오류를 남발하는 코드를 작성할 것이다.