Go를 배워보자 11일차 - 인터페이스, 인터페이스란? 인터페이스 정의

0

Go

목록 보기
11/12

인터페이스

1. 인터페이스

자바에서 객체 지향과 디자인 패턴의 핵심은 인터페이스이다. go 역시도 인터페이스를 지원하는 아주 재밌는 언어이다.

인터페이스를 사용하는 가장 큰 이유는 어떤 값이 어떤 특징 타입을 갖는지 관심이 없는 경우가 있기 때문이다.

그 값이 무엇인지보다는 그 값으로 어떤 일을 할 수 있는 지에 대해서 더 관심을 두는 경우가 있다.

즉, 어떤 값에서 특정 메서드를 호출할 수 있는 지가 주요 관심사인 것이다. 가령, 자판기가 있다면 버튼을 눌러서 음료수를 꺼내는게 우리의 관심사이지, 이 자판기가 어느 제조사에서 만들어졌는지는 중요하지 않다.

이럴 때 사용하는 것이 바로 인터페이스인 것이다.

1.1 동일한 메서드를 가진 서로 다른 타입

다음의 예제를 보도록 하자

package main

import "fmt"

type TapePlayer struct {
	Batteries string
}

func (t TapePlayer) Play(song string) {
	fmt.Println("playing", song)
}

func (t TapePlayer) Stop() {
	fmt.Println("stopped")
}

type TapeRecorder struct {
	Microphones int
}

func (t TapeRecorder) Play(song string) {
	fmt.Println("playing", song)
}

func (t TapeRecorder) Record() {
	fmt.Println("Recording")
}

func (t TapeRecorder) Stop() {
	fmt.Println("Stopped!!")
}

func playList(device TapePlayer, songs []string) {
	for _, song := range songs {
		device.Play(song)
	}
	device.Stop()
}

func main() {
	player := TapePlayer{}
	mixtape := []string{"first", "second", "third"}
	playList(player, mixtape)
}

뭔가 복잡한 것 같지만, 다음과 같다.

playList() 함수를 호출할 것인데, device로 TapePlayer 타입을 받은 다음, songs를 통해서 play시킬 곡명을 받는다.

그런데, 문제는 TapeRecorder 타입을 못받는다는 것이다. 그러기 위해서는 playList() 함수를 하나 더 만든다음 함수 이름도 달리하고 받는 매개변수를 TapePlayer에서 TapeRecorder로 바꾸어주어야 한다.

이는 매우 번거로운 작업이며, 심지어 playList() 함수의 내부 동작까지 같다.
즉 playList() 함수는 device의 Play 메서드와 Stop 메서드를 동작시키는데 입력으로 받는 device의 타입이 TapePlayer든 TapeRecorder 이든 상관없이 내부 동작은 똑같이 동작한다는 것이다.

단지 타입이 다르다는 이유로 똑같은 동작을 하는 다른 함수를 만들어야 한다는 것이다.

위에서 인터페이스를 사용하는 가장 큰 이유로 다음을 뽑았다.

인터페이스를 사용하는 가장 큰 이유는 어떤 값이 어떤 특징 타입을 갖는지 관심이 없는 경우가 있기 때문이다.

바로 인터페이스를 사용할 때가 온 것이다. playList() 함수의 device 매개변수는 어떤 특징 타입을 갖는 지 관심이 없다. apePlayer든 TapeRecorder 이든 상관없이 똑같이 동작하기 때문이다.

그렇다면 인터페이스는 어떻게 정의할까??

1.2 인터페이스 정의

Go에서 인터페이스는 특정 값이 가지고 있기를 기대하는 메서드의 집합으로 정의된다. 즉, 인터페이스는 동작을 수행할 수 있는 타입이 지녀야 하는 동작들의 집합이라고 생각할 수 있다.

정리하면 인터페이스는 특정 값이 가지고 있기를 기대하는 메서드 집합이다

인터페이스는 interface 키워드를 사용하여 정의할 수 있다. interface 키워드 다음으로는 메서드가 가지고 있기를 기대하는 매개변수 또는 반환 값과 함께 메서드 이름의 목록이 중괄호 안에 감싸여 따라온다.

type myInterface interface {
    methodWithoutParameters()
    methodWithParameter(float64)
    methodWithReturnValue() string
}

다음과 같이 interface 키워드를 struct키워드 대신에 사용하고, type는 여전히 똑같이 사용한다.

interface 안에 정의되는 메서드들은 반드시 매개변수와 리턴 타입을 명시해야 한다.

인터페이스 정의에 나열된 모든 메서드를 가진 타입은 해당 인터페이스를 만족한다고 한다. 인터페이스를 만족하는 타입은 해당 인터페이스가 필요한 모든 곳에서 사용할 수 있다.

인터페이스를 만족하려면 인터페이스에 정의된 메서드명, 매개변수 타입, 그리고 반환 값 타입이 모두 일치해야 한다.

타입은 여러 인터페이스를 만족할 수 있으며, 인터페이스 또한 인터페이스를 만족하는 여러 타입을 가질 수 있다.

다음의 예시를 보자

package main

import "fmt"

type myInterface interface {
	MethodWithoutParameters()
	MethodWithParameter(float64)
	MethodWithReturnValue() string
}

type MyType int

func (m MyType) MethodWithoutParameters() {
	fmt.Println("MethodWithoutParameters called")
}

func (m MyType) MethodWithParameter(input float64) {
	fmt.Println("MethodWithParameter called", input)
}

func (m MyType) MethodWithReturnValue() string {
	fmt.Println("MethodWithReturnValue called")
	return "Done"
}

func main() {
	var value myInterface
	value = MyType(5)
	value.MethodWithoutParameters()      // MethodWithoutParameters called
	value.MethodWithParameter(123.7)     // MethodWithParameter called 123.7
	ret := value.MethodWithReturnValue() // MethodWithReturnValue called
	fmt.Println(ret)
}

재밌는 것은 java처럼 interface를 implements하고 있다는 명시적인 선언이 없다. Go에서는 자동으로 처리되기 때문에 어떤 타입이 특정 인터페이스에 선언된 모든 메서드를 구현하고 있으면 추가로 선언하지 않아도 해당 인터페이스가 필요한 모든 곳에서 사용할 수 있다.

따라서, 인터페이스의 모든 메서드들을 구현한 타입은 이제 해당 인터페이스 타입을 가진 변수에 할당될 수 있다. 그래서 value 변수에 MyType 타입의 값이 들어갈 수 있는 것이다.

재밌는 것은 java에서는 인터페이스의 구현체가 되면 모든 메서드들을 구현했어야 했는데 go에서는 특정 메서드만 구현할 수 있다. 다만, 이런 경우에는 인터페이스 타입과 연동이 안된다. 즉, 위의 예제에서 value에 MyType 타입을 가진 변수가 들어가질 못한다는 것이다.

package main

import "fmt"

type myInterface interface {
	MethodWithoutParameters()
	MethodWithParameter(float64)
	MethodWithReturnValue() string
}

type MyType int

func (m MyType) MethodWithoutParameters() {
	fmt.Println("MethodWithoutParameters called")
}

func (m MyType) MethodWithParameter(input float64) {
	fmt.Println("MethodWithParameter called", input)
}

func main() {
	temp := MyType(5)
	temp.MethodWithoutParameters()  // MethodWithoutParameters called
	temp.MethodWithParameter(123.7) // MethodWithParameter called 123.7

}

해당 경우처럼 일부만 구현할 수 있지만 이는 인터페이스의 구현체로서의 역할을 못하므로 인터페이스에 값을 할당할 수 없다.

  • 구체 타입 , 인터페이스 타입
    이전에 배운 모든 타입은 구체 타입이었다. 구체 타입(concrete type)은 어떤 값이 무엇을 할 수 있는 지뿐만 아니라 그 값이 무엇인지를 정의한다. 즉, 구체 타입에서는 값의 데이터가 저장될 기본 타입을 저장한다.

    반면 인터페이스 타입은 값이 무엇인지를 기술하지 않는다. 기본 타입이 무엇인지, 어떻게 저장되는 지 아무런 정보도 기술하지 않으며 값이 무엇을 할 수 있는지만 기술한다. 즉, 어떤 메서드를 가지고 있는지만 기술할 뿐이다.

1.3 인터페이스를 만족하는 타입 할당하기

인터페이스 타입을 가진 변수는 인터페이스를 만족하는 모든 타입의 값을 가질 수 있다.

그럼 인터페이스에서 해당 가지고 있는 값의 메서드를 호출할 수 있을까?? 그건 불가능하다. 인터페이스 변수는 인터페이스에 정의된 메서드만 호출할 수 있다.

package main

import "fmt"

type NoiseMaker interface {
	MakeSound()
}

type Whistle string

func (w Whistle) MakeSound() {
	fmt.Println("Tweet")
}

type Horn string

func (h Horn) MakeSound() {
	fmt.Println("Honk")
}

type Robot string

func (r Robot) MakeSound() {
	fmt.Println("beepbo")
}

func (r Robot) Walk() {
	fmt.Println("powering legs")
}

func play(n NoiseMaker) {
	n.MakeSound()
}

func main() {
	play(Whistle("")) // Tweet
	play(Horn(""))    // Honk
}

다음의 예제에서는 인터페이스 타입이 인터페이스 조건을 만족하는 타입을 모두 받을 수 있다는 것을 알려주고 있다.

그러나, Robot의 경우 Walk()라는 메서드가 있는데 이를 호출할 수는 없을까??

위 예제에서 main 함수 부분을 다음으로 바꾸어보자

func main() {
	play(Whistle("")) // Tweet
	play(Horn(""))    // Honk

	var value NoiseMaker
	value = Robot("")
	value.MakeSound() // beepbo
	value.Walk() // value.Walk undefined
}

value.Walk() 하면 에러가 발생할 것이다. 이는 인터페이스는 자신이 정의한 메서드만 호출할 수 있지, 그 안의 값까지 접근할 수 없다는 것을 의미한다. 이는 자바에서의 인터페이스와도 똑같은 특징이다.

마지막으로 인터페이스를 이용하여 맨 위의 예제를 수정해보도록 하자

package main

import "fmt"

type TapeInterface interface {
	Play(string)
	Stop()
}

type TapePlayer struct {
	Batteries string
}

func (t TapePlayer) Play(song string) {
	fmt.Println("playing", song)
}

func (t TapePlayer) Stop() {
	fmt.Println("stopped")
}

type TapeRecorder struct {
	Microphones int
}

func (t TapeRecorder) Play(song string) {
	fmt.Println("Recording", song)
}

func (t TapeRecorder) Record() {
	fmt.Println("Recording")
}

func (t TapeRecorder) Stop() {
	fmt.Println("Stopped!!")
}

func playList(device TapeInterface, songs []string) {
	for _, song := range songs {
		device.Play(song)
	}
	device.Stop()
}

func main() {
	player := TapePlayer{}
	record := TapeRecorder{}
	mixtape := []string{"first", "second", "third"}
	playList(player, mixtape)
	playList(record, mixtape)
}

type TapeInterface interface를 하나만들고 func playList(device TapeInterface, songs []string)에 넣어주어 TapePlayer와 TapeRecorder를 받을 수있도록 하였다.

결과는 다음과 같다.

playing first
playing second
playing third
stopped
Recording first
Recording second
Recording third
Stopped!!

뒤에 더 인터페이스에 대해서 배우겠지만 java에서 만큼 인터페이스가 뭔가 더 엄격하지 않다는 것을 느꼈을 것이다. 이것이 장점인지 단점인지는 상황에 따라 달라질 것이지만 형식적이기 보다는 실용적인 측면에서 코드를 만들었다는 느낌이 많이든다.

0개의 댓글