[Go] 13. 인터페이스

2024. 1. 21. 13:39Go

반응형

1. 인터페이스

인터페이스는 메서드 선언을 가지고 있는 타입으로, 아래와 같은 성질을 가지고 있습니다.

  • 메서드명은 필수 요소입니다
  • 동일한 메서드명은 설정할 수 없습니다. (overloading x)
  • 메서드 선언만 들어있습니다. (메서드 구현은 포함x)

type 키워드 뒤에 인터페이스명을 쓰고, interface 키워드를 쓴 후 중괄호를 쓰며
중괄호 안에 인터페이스에 포함된 메서드들을 씁니다

type InterfaceName interface {
  Introduce()
  calculate(a, b int) int
}

아래는 인터페이스를 사용한 예제코드입니다.

package main  

import "fmt"  

type Member interface {  
  Introduce() string  
}  

type VipMember struct {  
  Name string  
  Age  int  
}  

func (u VipMember) Introduce() string {  
  return fmt.Sprintf("이름: %s, 나이: %d", u.Name, u.Age)  
}  

func main() {  
  var member Member = VipMember{"coco", 25}  
  fmt.Println(member.Introduce())  
}
이름: coco, 나이: 25

인터페이스에 정의한 메서드를 로컬타입의 메서드로 선언해두면
인터페이스 타입인 member에 VipMember를 담을 수 있고,

Member인터페이스에는 Introduce 메서드에 대한 구현이 작성되어있지 않지만, 담겨져있는 타입이 VipMember이기 때문에
Introduce() 메서드를 호출할 경우, VipMember에 정의한 Introduce 메서드가 호출되는 것입니다.

이러한 성질을 잘 활용하면, 구현 객체의 교체만으로 코드를 수정할 수 있게 되기 때문에 코드수정이 편리해집니다.


2. 인터페이스 사용 예제

2.1. fedex 모듈 생성

먼저, fedex 모듈을 생성해봅시다.

ch20디렉토리 하위에 fedex디렉토리를 만든 후, 그 안에 fedex.go 파일을 생성하여

cd ch20
mkdir fedex
vim fedex.go

아래와 같이 Send메서드를 정의해봅니다.

package fedex

import "fmt"

type FedexSender struct {
}

func (f *FedexSender) Send(parcel string) {
	fmt.Printf("Fedex sends %s parcel\n", parcel)
}

그 후, 모듈을 github 외부 레파지토리로 설정하여 모듈을 생성합니다.

go mod init github.com/jiniya22/goplayground/ch20/fedex

그러면 아래와 같이 go.mod 파일이 생성됩니다.

module github.com/jiniya22/goplayground/ch20/fedex

go 1.21.5

2.2. 메인모듈 생성

ch20 디렉토리로 이동한 후, fedex모듈을 활용할 ex20.2 모듈을 생성해봅니다.

mkdir ex20.2
vim ex20.2.go
package main

func main() {
}

ex.20.2.go 파일의 껍데기를 만든 후, ex20.2 를 모듈로 만듭니다.

go mod init goplayground/ex20.2

그후, 위에서 생성했었던 fedex 모듈을 활용하여 FedexSender를 사용하는 함수를 생성하고 사용해봅니다.

package main

import (
	"github.com/jiniya22/goplayground/ch20/fedex"
)

func SendBook(name string, sender *fedex.FedexSender) {
	sender.Send(name)
}
func main() {
	sender := &fedex.FedexSender{}
	SendBook("ABC 신나는 영어", sender)
	SendBook("너에게 보내는 편지", sender)
}

외부 저장소에 들어있는 모듈을 다운로드받습니다.

go mod tidy
go: finding module for package github.com/jiniya22/goplayground/ch20/fedex
go: downloading github.com/jiniya22/goplayground v0.0.0-20240114082621-0a685760f1d2
go: downloading github.com/jiniya22/goplayground/ch20/fedex v0.0.0-20240114082621-0a685760f1d2
go: found github.com/jiniya22/goplayground/ch20/fedex in github.com/jiniya22/goplayground/ch20/fedex v0.0.0-20240114082621-0a685760f1d2

모듈을 다운받은 후, go.sum 파일이 생성됩니다.

위의 모듈을 빌드한 후 실행시켜보면 아래와 같이 출력됩니다.

Fedex sends ABC 신나는 영어 parcel
Fedex sends 너에게 보내는 편지 parcel

2.3. koreapost 모듈 추가

fedex 모듈과 동일한 방식으로 koreapost(우체국) 모듈을 생성합니다.

package koreapost

import "fmt"

type PostSender struct {
}

func (f *PostSender) Send(parcel string) {
	fmt.Printf("우체국에서 택배 %s 를 보냅니다\n", parcel)
}

go mod init github.com/jiniya22/goplayground/ch20/koreapost


2.4. 인터페이스 활용

아래의 함수를, 인터페이스를 활용하여 FedexSender, PostSender가 모두 사용할 수 있는 함수로 변경해보자.

func SendBook(name string, sender *fedex.FedexSender) {
	sender.Send(name)
}
package main

import (
	"github.com/jiniya22/goplayground/ch20/fedex"
	"github.com/jiniya22/goplayground/ch20/koreapost"
)

type Sender interface {
	Send(parcel string)
}

func SendBook(name string, sender Sender) {
	sender.Send(name)
}
func main() {
	fedexSender := &fedex.FedexSender{}
	postSender := &koreapost.PostSender{}
	SendBook("ABC 신나는 영어", fedexSender)
	SendBook("너에게 보내는 편지", postSender)
}
Fedex sends ABC 신나는 영어 parcel
우체국에서 택배 너에게 보내는 편지 를 보냅니다

3. 덕타이핑

Go의 인터페이스는 덕타이핑(duck typing) 방식을 사용합니다.
덕타이핑 방식이란, 인터페이스의 구현 여부를 명시적으로 나타낼 필요없이, 인터페이스에 선언된 메서드를 구현하는 것 만으로도 그 인터페이스의 구현하는 타입으로 간주하는 것을 의미합니다.

Java에서는, 인터페이스에 선언되어있는 메서드들을 가지고 있는 클래스더라도, implements 키워드로 그 인터페이스를 구현했다고 명시적으로 설정해야만 구현클래스로 간주했지만 Go는 이런 명시가 필요하지 않습니다.

인터페이스를 사용하는 시점에, 해당 타입이 인터페이스에 정의된 메서드를 포함하고 있는지를 판단하며, 이러한 성질로 인해 개발에 편의성이 높습니다.


4. 인터페이스를 포함하는 인터페이스

인터페이스는 인터페이스를 포함할 수 있습니다.

또, 포함하고 있는 인터페이스에서 이름이 겹치는 경우에는 하나만 포함됩니다

type Reader interface {
	Read() (n int, err error)
	Close() error
}

type Writer interface {
	Write() (n int, err error)
	Close() error
}

type ReadWriter interface {
	Reader
	Writer
}

ReadWriter 인터페이스는 Read() (n int, err error), Close() error, Write() (n int, err error) 를 모두 포함한 타입의 경우에만 사용가능합니다.


5. 빈 인터페이스

빈 인터페이스(interface{}) 는 어떠한 메서드도 포함하고 있지 않기 때문에 모든 타입을 담을 수 있습니다.

빈 인터페이스는 int, float64, string, int8 등 다양한 타입들을 담을 수 있기 때문에 아래와 같이 활용할 수 있습니다.

package main

import "fmt"

func PrintValue(v interface{}) {
	switch t := v.(type) {
	case int:
		fmt.Printf("%d는 int 타입", t)
	case float64:
		fmt.Printf("%f는 float64 타입", t)
	case string:
		fmt.Printf("%s는 string 타입", t)
	default:
		fmt.Printf("%v는 int, float64, string 외의 타입", t)
	}
	fmt.Println()
}

func main() {
	PrintValue(32)
	PrintValue(3.14)
	PrintValue("지니")
	var a int8 = 9
	PrintValue(a)
}
32는 int 타입
3.140000는 float64 타입
지니는 string 타입
9는 int, float64, string 외의 타입

6. 인터페이스 기본값

인터페이스의 기본값은 nil 입니다

인터페이스에 선언된 메서드를 포함하고 있는 타입으로 초기화하지 않고 사용할 경우에는 panic: runtime error: invalid memory address or nil pointer dereference 메시지와 함께 런타임 에러가 발생되기 때문에 이 부분에 대해 유의해야 합니다.

인터페이스 변수가 초기화 되어있는지에 대해서는 컴파일시 확인되지 않기 때문에
인터페이스의 메서드를 사용하기 전에, 인터페이스가 nil이 아닌지를 반드시 체크해야 합니다.

package main

type Member interface {
	Introduce()
}

func main() {
	var member Member
	member.Introduce()
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x102e3a7fc]

goroutine 1 [running]:
main.main()
        /Users/sherry/jini_box/jinispaces/go/goplayground/ch20/ex20.4/ex20.4.go:9 +0x1c

7. 인터페이스 변환

7.1. 인터페이스를 구체화타입으로 변환

VipMember 타입으로 생성한 포인터변수를 Member 인터페이스에 담을 수 있고,
이 member에 Introduce()를 실행하면 VipMember에 정의한 메서드가 실행됩니다.

인터페이스를 다시 *VipMember 타입으로 변환하고 싶다면 member.(*VipMember) 와 같은 방식으로 변환할 수 있습니다.

package main

import "fmt"

type Member interface {
	Introduce() string
}

type VipMember struct {
	Name string
	Age  int
}

func (v *VipMember) Introduce() string {
	return fmt.Sprintf("%s는 Vip회원이며 나이는 %d 입니다.", v.Name, v.Age)
}

func main() {
	var member Member = &VipMember{"sol", 26}
	fmt.Println(member.Introduce())
	fmt.Printf("member의 타입은 %T\n\n", member)

	vipMember := member.(*VipMember)
	fmt.Println(vipMember.Introduce())
	fmt.Printf("vipMember의 타입은 %T\n\n", vipMember)
}
sol는 Vip회원이며 나이는 26 입니다.
member의 타입은 *main.VipMember

sol는 Vip회원이며 나이는 26 입니다.
vipMember의 타입은 *main.VipMember

VipMember 타입 외에 BasicMember 타입을 만들고, 이 타입도 Member 인터페이스에 선언된 메서드를 구현한 후 아래 코드를 실행하면,

*VipMember 타입으로 선언한 member 변수가 *VipMember 로는 다시 변환하는게 가능하지만
*BasicMember 타입으로는 변환이 되지 않는 것을 알 수 있습니다.

package main

import "fmt"

type Member interface {
	Introduce() string
}

type BasicMember struct {
	Name string
	Age  int
}

func (v *BasicMember) Introduce() string {
	return fmt.Sprintf("%s는 기본회원이며 나이는 %d 입니다.", v.Name, v.Age)
}

type VipMember struct {
	Name string
	Age  int
}

func (v *VipMember) Introduce() string {
	return fmt.Sprintf("%s는 Vip회원이며 나이는 %d 입니다.", v.Name, v.Age)
}

func main() {
	var member Member = &VipMember{"sol", 26}
	fmt.Println(member.Introduce())
	fmt.Printf("member의 타입은 %T\n\n", member)

	vipMember := member.(*VipMember)
	fmt.Println(vipMember.Introduce())
	fmt.Printf("vipMember의 타입은 %T\n\n", vipMember)

	basicMember := member.(*BasicMember) // Runtime Exception 발생!
	fmt.Println(basicMember.Introduce())
	fmt.Printf("normalMember의 타입은 %T", basicMember)
}

basicMember := member.(*BasicMember) 실행 중에 panic: interface conversion: main.Member is *main.VipMember, not *main.BasicMember 에러가 발생됩니다.

다른 인터페이스 타입으로 변환하고 싶다면 위와 같은 방법으로는 불가합니다.


7.2. 인터페이스 타입 변환

인터페이스 타입 변환을 이용하여 다른 구체화타입/인터페이스로 변경할 수 있습니다.

인터페이스 타입을 변환한 후, 그 인터페이스에 선언된 메서드를 실행하는 부분에서 변환하는 인터페이스에 선언된 메서드가 포함되어있는지 유무를 컴파일시점에 판단하지 않기 때문에, 이에 대한 방어코드를 반드시 작성해야합니다

Shape 인터페이스와, 그 인터페이스를 구현한 Circle 타입이 있고,

type Shape interface {
	Introduce() string
}

type Circle struct {
	Name   string
	Radius int
}

func (v *Circle) Introduce() string {
	return fmt.Sprintf("%s는 원. 반지름은 %d 입니다.", v.Name, v.Radius)
}

Shape 인터페이스를 포함하고 있는 Polygon 인터페이스와
그 인터페이스를 구현한 Square 타입이 있다고 할 때,

type Polygon interface {
	Shape
	GetLines() int
}

type Square struct {
	Name  string
	Width int
}

func (v *Square) Introduce() string {
	return fmt.Sprintf("%s는 정사각형. 변의 길이는 %d 입니다.", v.Name, v.Width)
}

func (v *Square) GetLines() int {
	return 4
}

Shape 인터페이스를 Polygon 인터페이스로 변환하려 할때,
변환중 오류가 발생하지 않았을 때에만 특정 기능을 수행하도록 아래와 같이 작성할 수 있습니다.

func PrintIntroduce(s Shape) {
	p, ok := s.(Polygon)
	if ok {
		fmt.Printf("변의 수는 %d\n", p.GetLines())
	}
	fmt.Println(s.Introduce())
}

변환 중 오류가 발생하지 않았을 때에 특정 기능이 수행하도록 하는 코드는 아래와 같이 더 축약해서 작성할 수 있습니다.

if p, ok := s.(Polygon); ok {
	fmt.Printf("변의 수는 %d\n", p.GetLines())
}
func main() {
	var s1 Shape = &Circle{"원1", 2}
	fmt.Println(s1.Introduce())
	fmt.Printf("s1의 타입은 %T\n", s1)

	var s2 Shape = &Square{"사각형1", 5}
	fmt.Println(s2.Introduce())
	fmt.Printf("s2의 타입은 %T\n", s2)

	fmt.Println("-------")
	PrintIntroduce(s1)
	PrintIntroduce(s2)
}
원1는 원. 반지름은 2 입니다.
s1의 타입은 *main.Circle
사각형1는 정사각형. 변의 길이는 5 입니다.
s2의 타입은 *main.Square
-------
원1는 원. 반지름은 2 입니다.
변의 수는 4
사각형1는 정사각형. 변의 길이는 5 입니다.

위의 예제의 full code는 jiniya22 Github ex20.6.go에 있습니다.

728x90
반응형

'Go' 카테고리의 다른 글

[Go] 15. 자료구조 - list, ring, map  (0) 2024.02.05
[Go] 14. 함수 고급편  (0) 2024.02.04
[Go] 12. 메서드  (0) 2024.01.21
[Go] 10. 패키지  (0) 2024.01.20
[Go] 09. 문자열  (0) 2024.01.18