[Golang] Interface

2021-11-17 hit count image

Golang에서 Interface(인터페이스)에 대한 개념을 확인하고, 사용하는 방법에 대해서 알아봅시다.

개요

이번 블로그 포스트에서는 Golang의 Interface(인터페이스)에 대해 자세히 알아보고 사용하는 방법에 대해서 살펴보려고 합니다. 이 블로그 포스트에서 소개하는 코드는 다음 링크를 통해 확인하실 수 있습니다.

인터페이스

Golang에서 인터페이스는 구체화된 객체(Concrete object)가 아닌 추상화된 상호 작용으로, 관계를 표현하는데 사용합니다.

Golang에서는 다음과 같이 인터페이스를 선언할 수 있습니다.

type INTERFACE_NAME interface {
  METHOD_NAME(PARAMETER_NAME) RETURN_TYPE
  ...
}

Golang에서는 타입 선언 키워드(type)를 사용하여 인터페이스를 선언하며, 인터페이스명 뒤에 인터페이스 선언 키워드(interface)를 추가하여 인터페이스를 정의합니다.

인터페이스안에는 구현이 없는 메서드(메서드명, 파라메터, 리턴 타입만 선언)를 선언하며, 이렇게 선언된 메서드들을 가지고 있는 타입을 우리가 정의한 인터페이스로 인식하겠다는 것을 의미합니다.

Golang에서는 인터페이스도 하나의 타입이며, 인터페이스로 변수를 선언할 수도 있습니다.

이를 확인하기 위해 main.go 파일을 생성하고 다음과 같이 수정합니다.

package main

import "fmt"

type SampleInterface interface {
  SampleMethod()
}

func main() {
  var s SampleInterface
  fmt.Println(s)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
<nil>

인터페이스는 내부 동작을 감추는 추상화(Abstraction)을 위해 사용되며, 추상화를 통해 의존성을 제거하는 디커플링에 주로 사용됩니다.

인터페이스 기본값

Golang에서 인터페이스의 기본값은 nil입니다. 이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type SampleInterface interface {
  SampleMethod()
}

func main() {
  var s SampleInterface
  fmt.Println(s)
  s.SampleMethod()
}

이렇게 프로그램을 작성하였다면, 다음 명령어를 실행하여 모듈을 생성하고 프로그램을 빌드해 봅니다.

go mod init github.com/dev-yakuza/study-golang/interface/nil
go mod tidy
go build

이렇게 빌드하면, main.go 파일이 위치한 곳에 nil이라는 이름의 파일이 생성된 것을 확인할 수 있습니다.

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# ./nil
<nil>
panic: runtime error: invalid memory address or nil pointer dereference

위에 예제는 문법적으로 오류가 없기 때문에 빌드가 됩니다. 하지만 인터페이스 변수에 값이 설정되어 있지 않기 때문에 런타임 에러(nil pointer 에러)가 발생하는 것을 확인할 수 있습니다.

Golang에서 모듈을 사용하는 방법에 대해서는 아래에 블로그 포스트를 참고하시기 바랍니다.

인터페이스 규칙

Golang에서 인터페이스는 다음과 같은 규칙을 가지고 있습니다.

  1. 메서드는 반드시 메서드명이 있어야 한다.
  2. 매개 변수와 반환이 다르더라도 이름이 같은 메서드는 있을 수 없다.
  3. 인터페이스에서는 메스드 구현을 포함하지 않는다.
type SampleInterface interface {
  String() string
  String(a int) string // Error: duplicated method name
  _(x int) int         // Error: no name method
}

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type SampleInterface interface {
  String() string
  String(a int) string // Error: duplicated method name
  _(x int) int         // Error: no name method
}

func main() {
  var s SampleInterface
  fmt.Println(s)
}

이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

# go run main.go
# command-line-arguments
./main.go:7:2: duplicate method String
./main.go:8:2: methods must have a unique non-blank name

예제

간단한 예제를 만들어 보면서 Golang에서의 인터페이스를 이해해 봅시다. main.go 파일을 생성하고 다음과 같이 수정합니다.

package main

import "fmt"

type Human interface {
  Walk() string
}

type Student struct {
  Name string
  Age  int
}

func (s Student) Walk() string {
  return fmt.Sprintf("%s can walk", s.Name)
}

func (s Student) GetAge() int {
  return s.Age
}

func main() {
  s := Student{Name: "John", Age: 20}
  var h Human

  h = s
  fmt.Println(h.Walk())
  // fmt.Println(h.GetAge()) // ERROR
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
John can walk

소스 코드를 자세히 보면, Human이라는 인터페이스는 Walk라는 함수를 가지고 있습니다.

type Human interface {
  Walk() string
}

그리고 Student 구조체 타입은 NameAge라는 변수를 가지고 있으며, WalkGetAge라는 메서드를 가지고 있습니다.

type Student struct {
  Name string
  Age  int
}

func (s Student) Walk() string {
  return fmt.Sprintf("%s can walk", s.Name)
}

func (s Student) GetAge() int {
  return s.Age
}

이제 Student 구조체를 사용하여 변수를 생성하고, 해당 변수를 Human 인터페이스에 할당하였습니다.

func main() {
  s := Student{Name: "John", Age: 20}
  var h Human

  h = s
  fmt.Println(h.Walk())
  // fmt.Println(h.GetAge()) // ERROR
}

이렇게 할당된 Human 변수는 Walk라는 함수를 가지고 있으므로, Human 변수의 Walk 함수를 호출하는 것은 가능하지만, GetAge 함수를 호출할 수 없는 것을 알 수 있습니다.

덕 타이핑

Golang의 인터페이스는 덕 타이핑(Duck typing)을 구현하였습니다. 덕 타이핑이란 만약 어떤 새를 봤는데, 그 새가 오리처럼 걷고, 오리처럼 날고, 오리처럼 소리를 낸다면, 그 새는 오리라고 부르겠다라는 의미를 가지고 있습니다.

즉, 어떤 객체가 어떤 변수를 가지고 있고, 어떤 함수를 가지고 있는지와 관계없이, 해당 객체를 사용하는 쪽에서, 이런 함수들을 가지고 있다면, 이런 타입으로 보겠다고 정의할 수 있습니다.

예제를 통해 이 내용을 확인해 봅시다. 우선 user/main.go 파일을 생성하고 다음과 같이 수정합니다.

package user

import "fmt"

type User struct {
  Name string
}

func (u User) Walk() {
  fmt.Println("Walking")
}
func (u User) Talk() {
  fmt.Println("Talking")
}

그리고 다음과 같이 모듈을 생성합니다.

# cd user
go mod init github.com/dev-yakuza/study-golang/interface/user
go mod tidy

이렇게 생성한 user 패키지는 User 타입을 가지고 있으며, 해당 User 타입은 Walk 메서드와 Talk 메서드를 가지고 있습니다. 이제 이렇게 생성한 user 패키지를 인터페이스를 통해 덕 타이핑을 해 봅시다.

덕 타이핑을 하기 위해 user 폴더와 동일한 위치에 main/main.go 파일을 생성하고 다음과 같이 수정합니다.

package main

import (
  "fmt"

  "github.com/dev-yakuza/study-golang/interface/user"
)

type Human interface {
  Walk()
  Talk()
}

func main() {
  u := user.User{Name: "John"}
  fmt.Println(u)

  h := Human(u)
  fmt.Println(h)
}

그리고 다음 명령어를 실행하여 모듈을 생성하고, 로컬 모듈과 연동합니다.

go mod init github.com/dev-yakuza/study-golang/interface/main
go mod edit -replace github.com/dev-yakuza/study-golang/interface/user=../user
go mod tidy

이렇게 생성한 모듈을 실행하면, 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
{John}
{John}

main/main.go 파일의 내용을 보면 User 타입이 어떤 타입인지 어떻게 구현되었는지 상관없이 구현된 타입이 WalkTalk를 가지고 있다면, main 패키지에서는 이를 Human으로 보겠다고 정의하였습니다.

이렇게 인터페이스는 외부 패키지를 사용할 때, 자신의 코드에 맞게 객체를 변경하여 사용할 수 있습니다. 외부 패키지를 구현하는 입장에서는 타입과 메서드를 구현할 때, 인터페이스를 고려할 필요가 없습니다. 그러므로, 인터페이스의 함수가 해당 타입에 구현이 되어 있지 않는 경우가 발생할 수 있으며, 이런 경우에는 컴파일 에러가 발생하게 됩니다.

덕 타이핑은 코드를 사용하는 사용자 중심으로 코딩이 가능하도록 해 줍니다. 외부 패키지 제공자는 구체화된 객체를 제공하고, 이를 사용하는 사용자는 필요에 따라 인터페이스를 정의해서 자신의 프로그램에 맞게 사용할 수 있습니다.

Embedded interface

Golang에서 인터페이스는 다음과 같이 인터페이스를 포함할 수 있습니다. 이를 Golang에서 Embedded interface라고 부릅니다.

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 인터페이스는 ReaderWriter 인터페이스를 모두 포함하고 있으며, Read, Write, Close 함수를 가지게 됩니다.

타입 변환

Golang에서는 다음과 같이 인터페이스를 타입(ConcreteType)으로 변환할 수 있습니다.

var a Interface
t := a.(ConcreteType)

인터페이스에서 타입으로 변환할 때 .(타입)을 사용합니다. 위와 같이 사용하면 ConcreteType 타입의 변수를 생성하여 t에 저장합니다.

인터페이스를 타입으로 변환할 때, 같은 인터페이스 타입인 경우, 타입 변환은 가능하지만, 해당 타입이 가지고 있지 않은 변수나 함수를 실행하면, 런타임 에러가 발생합니다.

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type Human interface {
  Learn()
}

type Teacher struct {
  Name string
}

func (m *Teacher) Learn() {
  fmt.Println("Teacher can learn")
}

func (m *Teacher) Teach() {
  fmt.Println("Teacher can teach")
}

type Student struct {
  Name string
}

func (m *Student) Learn() {
  fmt.Println("Student can learn")
}

func Study(h Human) {
  if h != nil {
    h.Learn()

    var s *Student
    s = h.(*Student)
    fmt.Printf("Name: %v\n", s.Name)
    // s.Teach() // ERROR
  }
}

func main() {
  Study(&Student{Name: "John"})
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
Student can learn
Name: John

인터페이스 타입 변환시 변환하고자 하는 타입이 해당 인터페이스가 아닌 경우 컴파일 에러가 발생합니다. 이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type Stringer interface {
  String() string
}

type Student struct {
}

func main() {
  var stringer Stringer
  s := stringer.(*Student)
  fmt.Println(s)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
# command-line-arguments
./main.go:14:15: impossible type assertion:
        *Student does not implement Stringer (missing String method)

다른 인터페이스 타입으로 변환

Golang에서는 인터페이스를 다른 인터페이스로 타입 변환이 가능합니다. 문법적으로는 전혀 문제가 없지만, 실제로 실행할 시, 변경된 인터페이스에 해당 함수가 없어 런타임 에러가 발생할 수 있습니다.

이를 확인하기 위해 main.go 다음과 같이 수정합니다.

package main

import "fmt"

type Teacher interface {
  Teach()
}

type Student interface {
  Learn()
}

type Person struct {
}

func (f *Person) Learn() {
  fmt.Println("Person can learn")
}

func Teach(s Student) {
  t := s.(Teacher)
  t.Teach()
}

func main() {
  p := &Person{}
  Teach(p)
}

이를 실행하면 다음과 같은 결과가 표시됩니다.

# go run main.go
panic: interface conversion: *main.Person is not main.Teacher: missing method Teach

Person 타입의 변수를 Student 인스턴스로 받고, 이렇게 할당 받은 Student 인터페이스를 Teacher 인터페이스로 변환한 뒤, Teacher 인터페이스의 Teach 함수를 호출했습니다. 문법적으로는 문제가 없기 때문에 컴파일 에러가 발생하지 않습니다. 하지만, 실제로 실행을 하면 Person 타입에는 Teach 함수가 없기 때문에 에러가 발생합니다.

Golang은 이런 문제를 방지하기 위해 타입 변환이 성공했는지 여부를 체크하는 기능을 제공하니다.

타입 변환 성공 여부

인터페이스의 타입 변환은 문법적으로 오류가 아니기 때문에, 빌드시 에러가 발생하지 않습니다. 하지만, 런타임시 에러가 날 가능성이 있습니다. 이를 방지하기 위해, Golang은 타입 변환시 타입이 제대로 변경되었는지 여부를 알 수 있는 방법이 제공하고 있습니다.

var a Interface
t, ok := a.(ConcreteType)
  • t: 타입 변환 결과
  • ok: 변환 성공 여부

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type Teacher interface {
  Teach()
}

type Student interface {
  Learn()
}

type Person struct {
}

func (f *Person) Learn() {
  fmt.Println("Person can learn")
}

func main() {
  p := &Person{}
  s := Student(p)
  s.Learn()

  t, ok := s.(Teacher)
  fmt.Println(ok)
  if ok {
    t.Teach()
  }
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
Person can learn
false

Person 타입은 Student 인터페이스에 할당할 수 있지만, Student 인터페이스는 Teach 함수가 없기 때문에 Teacher 인터페이스로 변경이 불가능합니다.

따라서 인터페이스 타입 변환을 시도하였을 때, 변환 결과가 false인 것을 확인할 수 있습니다.

이와 같이 인터페이스의 타입 변환이 잘 되었는지 확인할 수 있으며, 인터페이스 타입 변환을 할 때에는 주로 다음과 같이 사용합니다.

if t, ok := s.(Teacher); ok {
  t.Teach()
}

빈 인터페이스

Golang에서는 빈 인터페이스를 활용할 때가 있습니다. 빈 인터페이스는 메서드를 가지고 있지 않기 때문에, 모든 타입을 빈 인터페이스로 변환할 수 있습니다.

interface {}

이런 빈 인터페이스는 다음과 같이 활용될 수 있습니다.

package main

import "fmt"

type Student struct {
  Age int
}

func Print(v interface{}) {
  switch v := v.(type) {
  case int:
    fmt.Println("v is int", v)
  case float64:
    fmt.Println("v is float64", v)
  case string:
    fmt.Println("v is string", v)
  default:
    fmt.Printf("Not supported %T:%v", v, v)
  }
}

func main() {
  Print(10)
  Print(3.14)
  Print("Hello word")
  Print(Student{Age: 10})
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
v is int 10
v is float64 3.14
v is string Hello word
Not supported main.Student:{10}%

완료

이것으로 Golang에서 인터페이스란 무엇이고, 인터페이스를 사용하는 방법에 대해서 알아보았습니다. 또한 인터페이스를 사용한 덕 타이핑과 타입 변환에 대해서도 알아보았습니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

책 홍보

스무디 한 잔 마시며 끝내는 React Native 책을 출판한지 벌써 2년이 다되었네요.
이번에도 좋은 기회가 있어서 스무디 한 잔 마시며 끝내는 리액트 + TDD 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
Posts