ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Golang] 인터페이스 (Interface)
    언어/Golang 2017. 2. 20. 20:19


    구조체(struct)가 필드들의 집합체라면, interface는 메서드들의 집합체입니다. interface는 타입(type)이 구현해야 하는 메서드 원형(prototype)들을 정의합니다. 하나의 사용자 정의 타입이 interface를 구현하기 위해서는 단순히 그 인터페이스가 갖는 모든 메서드들을 구현하면 됩니다. 인터페이스의 이름은 보통 변수에 ~er이 붙습니다. ex) printer interface {}

    인터페이스는 struct와 마찬가지로 type 문을 사용하여 정의합니다.

    type Shape interface {
        area() float64
        perimeter() float64
    }

    구현

    예제 1

    인터페이스를 구현하기 위해서는 해당 타입이 그 인터페이스의 메서드들을 모두 구현하면 되므로, 위의 Shape 인터페이스를 구현하기 위해서는 area(), perimeter() 2개의 메서드만 구현하면 됩니다. 예를 들어 Rect와 Circle이라는 2개의 타입이 있을 때, Shape 인터페이스를 구현하기 위해서는 아래와 같이 각 타입별로 2개의 메서드를 구현해 주면 됩니다.

    //Rect 정의
    type Rect struct {
        width, height float64
    }
     
    //Circle 정의
    type Circle struct {
        radius float64
    }
     
    //Rect 타입에 대한 Shape 인터페이스 구현 
    func (r Rect) area() float64 { return r.width * r.height }
    func (r Rect) perimeter() float64 {
         return 2 * (r.width + r.height)
    }
     
    //Circle 타입에 대한 Shape 인터페이스 구현 
    func (c Circle) area() float64 { 
        return math.Pi * c.radius * c.radius
    }
    func (c Circle) perimeter() float64 { 
        return 2 * math.Pi * c.radius

    예제 2

    package main
    
    import "fmt"
    
    type MyInt int // int 형을 MyInt로 정의
    
    func (i MyInt) Print() { // MyInt에 Print 메서드를 연결
    	fmt.Println(i)
    }
    
    type Rectangle struct { // 사각형 구조체 정의
    	width, height int
    }
    
    func (r Rectangle) Print() { // Rectangle에 Print 메서드를 연결
    	fmt.Println(r.width, r.height)
    }
    
    type Printer interface { // Print 메서드를 가지는 인터페이스 정의
    	Print()
    }
    
    func main() {
    	var i MyInt = 5
    	r := Rectangle{10, 20}
    
    	var p Printer // 인터페이스 선언
    
    	p = i     // i를 인터페이스 p에 대입
    	p.Print() // 5: 인터페이스 p를 통하여 MyInt의 Print 메서드 호출
    
    	p = r     // r을 인터페이스 p에 대입
    	p.Print() // 10 20: 인터페이스 p를 통하여 Rectangle의 Print 메서드 호출
    }

    MyInt 자료형, Ractangle 구조체, Printer 인터페이스를 그림으로 표현하면 다음과 같습니다. MyInt를 정의하고, 너비와 높이를 가지는 사각형 구조체 Rectangle을 정의했습니다. 그리고 MyInt, Rectangle 모두 자신의 내용을 출력하는 Print 함수를 구현했습니다. 이제 두 타입 모두 똑같은 Print 함수를 가지고 있습니다(여기서 똑같은 함수라는 것은 함수 이름, 매개변수 자료형, 리턴값 자료형이 모두 같은 상태를 뜻합니다).

    사용

    인터페이스를 사용하는 일반적인 예로 함수가 파라미터로 인터페이스를 받아들이는 경우를 들 수 있습니다. 함수 파라미터가 interface인 경우, 이는 어떤 타입이든 해당 인터페이스를 구현하기만 하면 모두 입력 파라미터로 사용될 수 있다는 것을 의미합니다.

    아래 예제에서 showArea() 함수는 Shape 인터페이스들을 파라미터로 받아들이고 있는데, 따라서 Rect와 Circle 처럼 Shape 인터페이스를 구현한 타입 객체들을 파라미터로 받을 수 있습니다. showArea() 함수 내에서 해당 인터페이스가 가진 메서드 즉 area() 혹은 perimeter()을 사용할 수 있습니다.

    func main() {
        r := Rect{10., 20.}
        c := Circle{10}
     
        showArea(r, c)
    }
     
    func showArea(shapes ...Shape) {
        for _, s := range shapes {
            a := s.area() //인터페이스 메서드 호출
            println(a)
        }
    }

    덕 타이핑

    이렇게 각 값이나 인스턴스의 실제 타입은 상관하지 않고 구현된 메서드로만 타입을 판단하는 방식을 덕 타이핑(Duck typing)이라 합니다. 이 용어는 다음과 같은 덕 테스트(오리 테스트)에서 유래되었습니다. “만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라 부르겠다.” Go 언어로는 다음과 같이 덕 타이핑을 구현할 수 있습니다.

    package main
    
    import "fmt"
    
    type Duck struct { // 오리(Duck) 구조체 정의
    }
    
    func (d Duck) quack() {     // 오리의 quack 메서드 정의
    	fmt.Println("꽥~!") // 오리 울음 소리
    }
    
    func (d Duck) feathers() { // 오리의 feathers 메서드 정의
    	fmt.Println("오리는 흰색과 회색 털을 가지고 있습니다.")
    }
    
    type Person struct { // 사람(Person) 구조체 정의
    }
    
    func (p Person) quack() {                           // 사람의 quack 메서드 정의
    	fmt.Println("사람은 오리를 흉내냅니다. 꽥~!") // 사람이 오리 소리를 흉내냄
    }
    
    func (p Person) feathers() { // 사람의 feathers 메서드 정의
    	fmt.Println("사람은 땅에서 깃털을 주워서 보여줍니다.")
    }
    
    type Quacker interface { // quack, feathers 메서드를 가지는 Quacker 인터페이스 정의
    	quack()
    	feathers()
    }
    
    func inTheForest(q Quacker) {
    	q.quack()    // Quacker 인터페이스로 quack 메서드 호출
    	q.feathers() // Quacker 인터페이스로 feathers 메서드 호출
    }
    
    func main() {
    	var donald Duck // 오리 인스턴스 생성
    	var john Person // 사람 인스턴스 생성
    
    	inTheForest(donald) // 인터페이스를 통하여 오리의 quack, feather 메서드 호출
    	inTheForest(john)   // 인터페이스를 통하여 사람의 quack, feather 메서드 호출
    }


    타입이 특정 인터페이스를 구현하는지 검사하려면 다음과 같이 사용합니다.

    • interface{}(인스턴스).(인터페이스)
    var donald Duck
    
    if v, ok := interface{}(donald).(Quacker); ok {
    	fmt.Println(v, ok)
    }
     
    # 결과
    # {} true

    Duck 타입의 인스턴스 donald를 빈 인터페이스에 넣은 뒤 Quacker 인터페이스와 같은지 확인합니다. 첫 번째 리턴값은 검사했던 인스턴스이며 두 번째 리턴값은 인스턴스가 해당 인터페이스를 구현하고 있는지 여부입니다. 구현하고 있다면 true 그렇지 않으면 false입니다.

    타입

    Go 프로그래밍을 하다보면 흔히 빈 인터페이스(empty interface)를 자주 접하게 되는데, 흔히 인터페이스 타입(interface type)으로도 불립니다. 예를 들어, 여러 표준 패키지들의 함수 Prototype을 살펴보면, 아래와 같이 빈 interface가 자주 등장함을 볼 수 있습니다. 빈 interface는 interface{} 와 같이 표현합니다.

    func Marshal(v interface{}) ([]byte, error);
     
    func Println(a ...interface{}) (n int, err error);


    Empty interface는 메서드를 전혀 갖지 않는 빈 인터페이스로서, Go의 모든 Type은 적어도 0개의 메서드를 구현하므로, 흔히 Go에서 모든 Type을 나타내기 위해 빈 인터페이스를 사용합니다. 즉, 빈 인터페이스는 어떠한 타입도 담을 수 있는 컨테이너라고 볼 수 있으며, 여러 다른 언어에서 흔히 일컫는 Dynamic Type 이라고 볼 수 있습니다. (주: empty interface는 C#, Java 에서 object라 볼 수 있으며, C/C++ 에서는 void* 와 같다고 볼 수 있음)

    아래 예제에서 인터페이스 타입 x는 정수 1을 담았다가 다시 문자열 Tom을 담고 있는데, 실행 결과는 마지막에 담은 Tom을 출력합니다.

    package main
     
    import "fmt"
     
    func main() {
        var x interface{}
        x = 1 
        x = "Tom"
     
        printIt(x)
    }
     
    func printIt(v interface{}) {
        fmt.Println(v) //Tom
    }

    Type Assertion

    Interface type의 x와 타입 T에 대하여 x.(T)로 표현했을 때, 이는 x가 nil이 아니며, x는 T 타입에 속한다는 점을 확인(assert)하는 것으로 이러한 표현을 "Type Assertion"이라 부릅니다. 만약 x가 nil 이거나 x의 타입이 T가 아니라면, 런타임 에러가 발생할 것이고, x가 T 타입인 경우는 T 타입의 x를 리턴합니다. 즉, 아래 예제에서 변수 j는 a.(int)로부터 int형 변수 j가 됩니다.

    func main() {
        var a interface{} = 1
     
        i := a       // a와 i 는 dynamic type, 값은 1
        j := a.(int) // j는 int 타입, 값은 1
     
        println(i)  // 포인터주소 출력
        println(j)  // 1 출력
    }


    댓글