프로그래밍 언어 & 프레임워크/Golang

Go: 인터페이스와 타입 단언 (Interface, Type Assertion)

인터페이스

인터페이스는 이기종간 기능을 약속하여 서로 다른 기기일지라도 오직 기능적인 관점에서 일관성을 유지하기 위해 사용한다. 같은 개발자라도 가지고 있는 기술에 따라 기술 스택이 달라지지만, 코딩이라는 기능은 가질 수 있는 것처럼 말이다.

 

인터페이스는 아래와 같이 정의하는데, Go 에서는 구조체에 정의하는 메서드에 대해 인터페이스를 사용하는 것을 명시하지 않는다. 인터페이스는 타입으로 선언하며 Develop 이라는 인터페이스는 코딩이라는 기능을 가질 수 있음을 말한다. 인터페이스는 실질적인 구현에 대해서는 이야기하지 않고, 오직 그 기능이 가능한 지에 대해서만 신경쓴다.

type Develop interface {
	Coding()
}

조건에 만족하려면

인터페이스를 만족하기 위한 조건은, 메서드를 가진 구조체가 인터페이스에 정의된 메서드를 충실하게 구현하고 있을 때를 말한다. Developer 구조체는 Coding() 이라는 메서드를 구현함으로써 Develop 인터페이스를 만족한다. 따라서 Develop 타입을 가진 변수에 대해 할당을 해줄 수 있게되는 것이다. 이렇게 인터페이스를 사용하게 되면 구현에 상관없이 오직 기능적인 관점에서 프로그램을 추상화하고 접근할 수 있다.

package main

import "fmt"

type Develop interface {
	Coding()
}

type Developer struct {
	Languages []string
}

func (d Developer) Coding() {
	for _, l := range d.Languages {
		fmt.Println(l)
	}
}

func main() {
	d := Developer{[]string{"Go, PHP, Javascript"}}
	d.Coding()
}

외부 의존성 줄이기

인터페이스 타입을 사용하여 함수나 메서드 파라매터의 타입으로 지정할 수 있다. 이렇게 하면 해당 인터페이스를 구현하고 있는 구체 타입을 받을 수 있다.

package main

import "fmt"

type Develop interface {
	Coding()
}

// ...

func Work(developer Develop) {
	developer.Coding()
}

func main() {
	d := Developer{[]string{"Go, PHP, Javascript"}}
	Work(d)
}

하지만 여기서 문제가 발생하는데, 인터페이스 타입으로 하는 경우, 구체 타입에 인터페이스에 선언된 메서드 이외에 다른 메서드가 있다면, 해당 메서드는 함수 내에서 일반적으로 호출할 수 없다. Work() 함수에서 developer 파라매터에 대해 OtherMethod() 메서드를 호출하려 들지만, Develop 인터페이스에는 해당 메서드가 없어서 에러가 난다.

package main

import "fmt"

// ...

func (d Developer) OtherMethod() {
}

func Work(developer Develop) {
	developer.Coding()
	//developer.OtherMethod() // -> Error
}

// ...

타입 단언

타입 단언을 사용하면 위에서 발생한 문제를 해결할 수 있다. 인터페이스 타입을 구체 타입으로 변환하고 싶은데, 일반적인 타입 변환으로는 불가능하다. 따라서 새로운 문법인 타입 단언을 사용해야 하는데, 이것은 인터페이스 타입이지만 넘어오는 값이 특정 타입인 경우에 대해 대응할 수 있다.

// ...

func Work(developer Develop) {
	developer.Coding()
	if developer, ok := developer.(Developer); ok {
		developer.OtherMethod()
	}
}

// ...

조건문을 자세히 살펴보자. developer.(Developer) 라는 형태로 사용하여 인터페이스 타입을 구체 타입으로 바꿀 수 있고, ok 에 변환 여부가 반환된다. 만약 developer 로 넘어온 값이 Developer 타입으로 변환할 수 없다면, 타입 단언은 실패하여 조건에 부합하지 않으므로 OtherMethod() 메서드는 실행하지 않는다.

포인터 리시버

인터페이스의 메서드를 구현할 때 포인터 리시버를 사용한다면, 인터페이스 타입을 가진 변수에 구체 타입을 할당할 때 주소 값으로 할당해야 올바르게 실행할 수 있다.

package main

import "fmt"

// ...

func (d *Developer) Coding() {
	for _, l := range d.Languages {
		fmt.Println(l)
	}
}

func main() {
	var d Develop = &Developer{[]string{"Go, PHP, Javascript"}}
	d.Coding()
}

빈 인터페이스

빈(empty) 인터페이스는 어떨 때 사용할 수 있을까? 애초에 인터페이스가 비어있다는 것이 무슨 말일까? fmt.Println() 과 같은 함수에 보면 다음과 같이 시그니처가 되어있는 모습을 볼 수 있다. 파라매터로 가변인자를 받으며, 타입은 interface{} 라고 되어있다.

$ go doc fmt Println
package fmt // import "fmt"

func Println(a ...interface{}) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline is
    appended. It returns the number of bytes written and any write error
    encountered.

interface{} 라고 표현되는 빈 인터페이스를 사용하면 모든 값을 받아서 처리할 수 있다. 하지만 메서드를 바로 사용할 수는 없으며 그러려면 타입 단언을 사용해야 한다. 이러한 빈 인터페이스는 상황에 따라 유용하게 쓸 수 있지만 만능은 아니라는 점을 기억해두자.