Go를 배워보자 12일차 - 인터페이스2, 타입 단언, 내장 인터페이스(error, Stringer, 빈 인터페이스)

0

Go

목록 보기
12/12

인터페이스 2

1. 타입 단언

이전에 interface 1편에서 interface 변수는 할당 받은 타입의 고유 메서드를 사용할 수 없다고 했다. 즉, 자바에서처럼 인터페이스와 구현체 모두 존재하는 메서드가 아닌, 구현체 내부에서만 존재하는 메서드는 인터페이스에서 사용할 수 없는 것처럼 말이다.

그렇다면 자바에서는 어떻게 했는가?? 타입을 변환해주면 되었다.
go에서의 인터페이스 역시도 타입을 변환하면 될 것 같다.

참고로 go에서의 타입 변환은 다음과 같다.

value_float := 1.2
value_int := int(value_float)

이전에 사용했던 예제를 가져와보자

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)
	}
	recorder := TapeRecorder(device)
	recorder.Record()
	device.Stop()
}

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

TapeInterface 인터페이스로 선언된 device를 TapeRecorder 타입을 가지는 recorder로 변환하다음, TapeRecorder에서만 존재하는 고유 메서드 Record를 호출하고 싶다.

그러나, 위 코드를 돌려보면 에러가 발생할 것이다.

recorder := TapeRecorder(device)

해당 부분이 문제인데, 타입 변환은 인터페이스 타입에는 사용할 수 없기 때문이다.

그렇다면 어떻게 해결할 수 있을까??
이 때 사용하는 것이 타입 단언이다.


구체 타입의 값이 인터페이스 타입의 변수에 할당되었을 때 타입 단언을 사용하면 구체 타입의 값을 가져울 수 있다.

  • 타입 단언 방법
var noiseMaker NoiseMaker = Robot("")
var robot Robot = noiseMaker.(Robot)

다음과 같이noiseMaker.(Robot)로 사용하면 된다.
즉, 인터페이스.(구현체타입)이 되는 것이다.

마치 인터페이스에서 특정 메서드를 사용하는 방법과 같다. 이를 이용하여 위의 문제를 해결해보자

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)
	}
	recorder := device.(TapeRecorder)
	recorder.Record()
	device.Stop()
}

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

위의 코드는 타입 단언인 recorder := device.(TapeRecorder)을 이용하여 인터페이스의 타입을 구현체로 바꾸어주어서 특정 구현체에만 있는 메서드를 호출할 수 있게 되었다. 그러나 분명 컴파일 에러는 해결했지만, 다음과 같은 에러 메시지가 발생했을 것이다.

panic: interface conversion: main.TapeInterface is main.TapePlayer, not main.TapeRecorder

이와 같은 에러가 발생하는 이유가 무엇일까??
바로 TapeInterface에는 TapeRecorder 뿐만 아니라 TapePlayer도 들어가기 때문이다. TapePlayer 구현체가 들어갔는데, 이를 TapeRecorder로 변환이 안되기 때문에 문제가 발생한다. 때문에 panic이 발생하는 것이다. 그렇다면 타입 단언이 실패할 때와 성공할 때를 구분하여 어떻게 코드를 유연하게 만들 수 있을까??

2. 타입 단언 실패 시 패닉 방지하기

panic은 컴파일 도중이 아닌 런타임 중에 발생한다.

이러한 타입 단언 실패를 찾아내기 위해서는 두 번째 반환값인 성공 여부를 확인하면 된다. bool 타입으로 반환되며, 실패 시에는 false, 성공 시에는 true로 반환된다.

var player Player = TapePlayer{}
recorder , ok := player.(TapeRecorder)
if ok {
    recorder.Record()
}else{
    fmt.Println("Player was not a TapeRecorder")
}

이를 이용하여 런타임 중의 패닉을 방지할 수 있는 것이다.

위의 panic으로 실패했던 예제를 고쳐보도록 하자

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

다음의 함수로 교체하면 문제없이 돌아가는 것을 확인할 수 있다.

3. 내장 인터페이스

3.1 error 인터페이스

이전에 fmt.Errorf() 함수를 통해서 err를 만들어냈었다. 여기서 err는 어떤 타입일까??

err := fmt.Errorf("error hello world")

여기서 Errorf 가 선언된 부분으로 가자

func Errorf(format string, a ...interface{}) error {}

다음과 같이 선언되었음을 확인할 수있다. 리턴 타입은 error로 들어가보면

type error interface {
 Error() string
}

다음과 같이 되어있다. 즉, error는 인터페이스이고, 메서드로 Error()만을 가지고 있는 것이다. 이를 이용하여 우리는 custom error 타입을 만들 수 있다.

실제로 error 인터페이스를 fmt 에 넣으면 Error() 메서드가 호출되어 동작한다. 다른 언어에서는 예외 처리가 아주 복잡한데 반해 go는 아주 간단하게 에러 처리 타입을 만들 수 있다는 것이다.

package main

import (
    "fmt"
)

type overHeatError float64

func (o overHeatError) Error() string {
    return fmt.Sprintf("over heat is : %0.2f", float64(o))
}

func checkTemperature(actual float64, criteria float64) error {
    excess := actual - criteria
    if excess > 0 {
        return overHeatError(excess)
    }
    return nil 
}

func main() {
    err := checkTemperature(38.5, 37.5)
    if err != nil {
        fmt.Println(err)
    }
}

다음과 같이 error 인터페이스를 구현하는 overHeapError을 만든 다음 리턴값으로 error 인터페이스를 사용할 수 있다. 이렇게 error 인터페이스의 구현체를 직접 구현하여 사용하면 에러가 어디에서 발생했는지 더 쉽게 따라갈 수 있어 좋다.

3.2 Stringer 인터페이스

자바에서는 toString() 메서드가 모두 상속되어있기 때문에 객체를 print 해도 문제없이 print가 된다.

그러나 golang은 사용자 정의 타입 모두 toString()을 상속하는 것은 아니다. 그렇기 때문에 이와 같은 기능을 하는 인터페이스를 구현하면 문제없이 작동할 수 있다. 그것이 Stringer 인터페이스 이다.

type Stringer interface {
    String() string
}

Stringer 인터페이스는 다음과 같이 정의되어 있다. 즉, String() string 메서드만 구현해주면 Stringer 인터페이스를 구현할 수 있는 것이다.

package main

import (
    "fmt"
)

type CoffeePot string

func (c CoffeePot) String() string {
    return string(c) + "coffee pot"
}

func main() {
    coffeePot := CoffeePot("cold brew")
    fmt.Println(coffeePot) // cold brewcoffee po
}

다음과 같이 Stringer 인터페이스의 String() 메서드를 구현해주기만 해도 Stringer 인터페이스의 구현체가 된다. 따라서 fmt의 메서드에 사용자 정의 타입이 들어가도 사용자가 직접 구현한 String() 메서드에 따라서 로그가 출력되는 것이다.

3.3 빈 인터페이스

재밌는 것은 fmt.Println() 안에 내 사용자 정의타입이 무엇이든 간에 모두 들어가는 것을 확인할 수 있었다. 굳이 Stringer 인터페이스를 구현하지 않아도 기본 적인 타입들도 파라미터로 들어갈 수 있는 것을 알 수 있다.

어떻게 그게 가능할까??
그건 모든 타입을 받는 인터페이스를 만들었기 때문이다. 그것이 빈 인터페이스 이다.

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

실제 fmt.Println() 메서드의 내부 선언 모습이다. interface가 있는데 딱히 무언가 선언된 인터페이스가 아니라, 가변 인자를 받는 interface{}로 리터럴하게 인터페이스가 선언된 것을 볼 수 있다.

이처럼 빈 인터페이스로 선언하면 어떤 타입이든 다 받을 수 있게 된다.

마치 자바의 Object처럼 말이다. 그냥 빈 인터페이스로 만들어주면 헷갈릴 수 있으므로 우리는 any 라는 인터페이스를 만들어서 어떤 타입이든 받을 수 있도록 해보자

type Any interface{}

바로 이렇게 Any를 빈 인터페이스로 만들어서 모든 타입을 받도록 하는 것이다. 이제 이를 이용해서 메서드와 타입들을 정의해보자

package main

import (
    "fmt"
)

type Any interface{}

type CoffeePot struct {
    name  string
    price int
}

func (c CoffeePot) String() string {
    return fmt.Sprintf("name : %s and price : %d", c.name, c.price)
}

type TeaPot struct {
    name  string
    price int
}

func (c TeaPot) String() string {
    return fmt.Sprintf("name : %s and price : %d", c.name, c.price)
}

func my_print(any Any) {
    fmt.Println("my ", any)
}

func main() {
    coffeePot := CoffeePot{name: "Coffee capsule", price: 10}
    teaPot := TeaPot{name: "Tea capsule", price: 12}
    any := []Any{coffeePot, teaPot}
    for _, value := range any {
        my_print(value)
    }
}

CoffeePot, TeaPot 을 만들고 이들은 Stringer 인터페이스를 구현하는 구현체이다. 또한 Any는 빈 인터페이스 이므로, CoffeePot, TeaPot 모두 Any의 구현체에 해당하여 Any 타입에 들어갈 수 있다.

이를 이용하여 my_print() 함수는 어떠한 타입을 받던간에 Stringer의 String 함수를 이용해 로그를 출력해준다. 이게 무슨 쓸모없는 함수냐 할 수도 있지만, 여러 서버나 부서 간의 통신이나 메시지 연결 구조를 만들 때, 로그가 서로 섞여 버리면 우리 부서꺼인지 다른 부서꺼인지 헷갈릴 때가 있다. 이를 위해 로그를 따로 출력해주는 함수를 부서마다 만들곤 한다.

coffeePot, teaPot 인스턴스를 만들고, Any 타입의 슬라이스에 넣어준다. 그리고 for문을 통해 my_print에 넣어주면 Any 인터페이스로 받아 자동으로 fmt.Println() 으로 넘어가게 된다.

만약, 빈 인터페이스를 통해 메서드를 호출하고 싶다면, 타입 단언을 이용하여 해당 타입의 메서드를 가져오면 된다.

0개의 댓글