Go의 인터페이스 사용

완료됨

Go의 인터페이스는 다른 형식의 동작을 나타내는 데 사용되는 데이터의 형식입니다. 인터페이스는 개체가 충족해야 하는 청사진 또는 계약과 유사합니다. 인터페이스를 사용하면 특정 구현에 연결되지 않은 코드를 작성하기 때문에 코드베이스가 보다 유연해지고 적응력이 향상됩니다. 따라서 프로그램의 기능을 신속하게 확장할 수 있습니다. 이 모듈에서 그 이유를 알아봅니다.

다른 프로그래밍 언어의 인터페이스와 달리 Go의 인터페이스는 암시적으로 충족됩니다. Go는 인터페이스를 구현하기 위한 키워드를 제공하지 않습니다. 다른 프로그래밍 언어의 인터페이스는 익숙하지만 Go는 처음 접하는 경우, 이 개념이 헷갈릴 수 있습니다.

이 모듈에서는 여러 가지 예제를 통해 Go의 인터페이스를 살펴보면서 최대한 활용하는 방법을 설명합니다.

인터페이스 선언

Go의 인터페이스는 청사진과 같습니다. 구체적 형식이 소유하거나 구현해야 하는 메서드만 포함하는 추상 형식입니다.

도형이 구현해야 하는 메서드를 나타내는 기하 도형 패키지에 인터페이스를 만들려고 합니다. 다음과 같이 인터페이스를 정의할 수 있습니다.

type Shape interface {
    Perimeter() float64
    Area() float64
}

Shape 인터페이스는 Shape을 고려하는 모든 형식에 Perimeter()Area() 메서드가 모두 있어야 함을 의미합니다. 예를 들어 Square 구조체를 만들 때 메서드 하나가 아니라 둘 다를 구현해야 합니다. 또한 인터페이스에는 해당 메서드(예: 도형의 둘레 및 면적 계산)에 대한 구현 세부 정보가 포함되지 않습니다. 단순한 계약입니다. 삼각형, 원, 사각형 같은 도형은 다양한 방법으로 면적과 둘레를 계산할 수 있습니다.

인터페이스 구현

앞에서 설명한 대로 Go는 인터페이스를 구현하는 키워드를 제공하지 않습니다. Go의 인터페이스는 인터페이스에 필요한 모든 메서드가 있으면 형식을 통해 암시적으로 충족됩니다.

다음 예제 코드에 표시된 대로 Shape 인터페이스의 메서드 두 개가 모두 있는 Square 구조체를 만들어 보겠습니다.

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

Square 구조체의 메서드 서명이 Shape 인터페이스의 서명과 어떻게 일치하는지 확인합니다. 그러나 다른 인터페이스는 이름은 다르지만 동일한 메서드를 사용할 수 있습니다. Go는 구체적인 형식이 구현하는 인터페이스를 언제 어떻게 알 수 있나요? Go는 런타임에 인터페이스를 사용할 때 알 수 있습니다.

인터페이스가 어떻게 사용되는지를 보여 주려면 다음과 같은 코드를 작성할 수 있습니다.

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

위의 프로그램을 실행하면 다음과 같이 출력됩니다.

main.Square
Area:  9
Perimeter: 12

현재는 인터페이스를 사용하는지 여부에 전혀 차이가 없습니다. Circle 같은 다른 형식을 만든 다음, 인터페이스가 유용한 이유를 살펴보겠습니다. Circle 구조체에 대한 코드는 다음과 같습니다.

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

이제 다음과 같이 main() 함수를 리팩터링하고 면적 및 둘레와 함께 수신되는 개체의 형식을 출력할 수 있는 함수를 만들어 보겠습니다.

func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
    fmt.Println()
}

printInformation 함수가 Shape을 매개 변수로 사용하는지 확인합니다. 이 함수에 Square 또는 Circle 개체를 보낼 수 있고, 출력은 다르더라도 작동합니다. 이제 main() 함수는 다음과 같이 표시됩니다.

func main() {
    var s Shape = Square{3}
    printInformation(s)

    c := Circle{6}
    printInformation(c)
}

c 개체에 대해 Shape 개체임을 지정하지 않습니다. 그러나 printInformation 함수에는 Shape 인터페이스에 정의된 메서드를 구현하는 개체가 필요합니다.

프로그램을 실행하면 다음과 같이 출력됩니다.

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

어떻게 오류가 발생하지 않고 수신되는 개체 형식에 따라 출력이 달라지는지 확인합니다. 출력의 개체 형식이 Shape 인터페이스에 대해 아무것도 표시하지 않음을 확인할 수도 있습니다.

인터페이스 사용의 장점은 Shape의 구현이나 모든 새 형식에 대해 printInformation 함수를 변경할 필요가 없다는 점입니다. 앞에서 설명한 대로 인터페이스를 사용할 때 코드가 더욱 유연해지고 더욱 쉽게 확장할 수 있습니다.

Stringer 인터페이스 구현

기존 기능을 확장하는 간단한 예는 다음과 같이 String() 메서드가 있는 인터페이스인 Stringer를 사용하는 것입니다.

type Stringer interface {
    String() string
}

fmt.Printf 함수는 이 인터페이스를 사용하여 값을 출력합니다. 즉, 다음과 같이 사용자 지정 String() 메서드를 작성하여 사용자 지정 문자열을 출력할 수 있습니다.

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

위의 프로그램을 실행하면 다음과 같이 출력됩니다.

John Doe is from USA
Mark Collins is from United Kingdom

보시다시피, 여러분은 사용자 지정 형식(구조체)을 사용하여 String() 메서드의 사용자 지정 버전을 작성했습니다. 이 기술은 Go에서 인터페이스를 구현하는 일반적인 방법이며, 앞으로 살펴볼 많은 프로그램에서 관련 예시를 확인할 수 있습니다.

기존 구현 확장

다음과 같은 코드가 있다고 가정하고 일부 데이터를 조작하는 Writer 메서드의 사용자 지정 구현을 작성하여 기능을 확장하려고 합니다.

다음 코드를 사용하면 GitHub API를 사용하여 Microsoft에서 세 개의 리포지토리를 가져오는 프로그램을 만들 수 있습니다.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=3")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    io.Copy(os.Stdout, resp.Body)
}

위의 코드를 실행하면 다음과 같이 출력됩니다(가독성을 위해 축소함).

[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

io.Copy(os.Stdout, resp.Body) 호출은 GitHub API 호출을 통해 가져온 콘텐츠를 터미널에 출력하는 호출입니다. 터미널에 표시되는 콘텐츠를 줄이는 고유한 구현을 작성하려고 합니다. io.Copy 함수의 소스를 보면 다음과 같이 표시됩니다.

func Copy(dst Writer, src Reader) (written int64, err error)

첫 번째 매개 변수인 dst Writer을(를) 자세히 살펴보면 Writer이(가) 인터페이스임을 알 수 있습니다.

type Writer interface {
    Write(p []byte) (n int, err error)
}

Copy이(가) Write 메서드을(를) 호출하는 위치를 찾을 때까지 io 패키지의 소스 코드를 계속 탐색할 수 있지만 지금은 탐색은 내버려두겠습니다.

Writer는 인터페이스이고 Copy 함수에 필요한 개체이기 때문에 Write 메서드의 사용자 지정 구현을 작성할 수 있습니다. 따라서 터미널에 출력되는 콘텐츠를 사용자 지정할 수 있습니다.

인터페이스를 구현하려면 먼저 사용자 지정 형식을 만들어야 합니다. 이 경우 다음과 같이 사용자 지정 Write 메서드만 작성하면 되기 때문에 빈 구조체를 만들 수 있습니다.

type customWriter struct{}

이제 사용자 지정 Write 함수를 작성할 준비가 되었습니다. 또한 JSON 형식의 API 응답을 Golang 개체로 구문 분석하는 구조체를 작성해야 합니다. JSON-to-Go 사이트를 사용하여 JSON 페이로드에서 구조체를 만들 수 있습니다. 따라서 Write 메서드는 다음과 같이 표시될 수 있습니다.

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

마지막으로 다음과 같이 사용자 지정 개체를 사용하도록 main() 함수를 수정해야 합니다.

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

프로그램을 실행하면 다음과 같이 출력됩니다.

microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

작성한 사용자 지정 Write 메서드 덕분에 출력이 향상됩니다. 프로그램의 최종 버전은 다음과 같습니다.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

사용자 지정 서버 API 작성

마지막으로 서버 API를 만들 때 유용할 수 있는 인터페이스의 다른 사용 사례를 살펴보겠습니다. 웹 서버를 작성하는 일반적인 방법은 net/http 패키지의 http.Handler 인터페이스를 사용하는 것이며, 다음과 같이 표시됩니다. 이 코드를 작성할 필요는 없습니다.

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

ListenAndServe 함수에는 http://localhost:8000 같은 서버 주소, 그리고 호출에 대한 응답을 서버 주소로 전달하는 Handler 인스턴스가 필요합니다.

다음 프로그램을 만들어 살펴보겠습니다.

package main

import (
    "fmt"
    "log"
    "net/http"
)

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

위의 코드를 살펴보기 전에 다음과 같이 실행해 보겠습니다.

go run main.go

출력이 표시되지 않으면 좋은 징후입니다. 이제 새 브라우저 창에서 http://localhost:8000을 열거나 터미널에서 다음 명령을 실행합니다.

curl http://localhost:8000

이제 다음과 같이 출력됩니다.

Go T-Shirt: $25.00
Go Jacket: $55.00

이전 코드를 천천히 검토하여 수행하는 작업을 이해하고 Go 인터페이스의 성능을 살펴보겠습니다. 먼저 float32 형식의 사용자 지정 형식과 나중에 사용할 String() 메서드의 사용자 지정 구현 작성 개념을 만드는 것부터 시작합니다.

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

그런 다음, http.Handler에서 사용할 수 있는 ServeHTTP 메서드의 구현을 작성했습니다. 사용자 지정 형식을 다시 만들었지만 이번에는 구조체가 아니라 맵입니다. 다음으로 database 형식을 수신자로 사용하여 ServeHTTP 메서드를 작성했습니다. 이 메서드의 구현에서는 수신자의 데이터를 사용하고 반복하여 각 항목을 출력합니다.

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

마지막으로 main() 함수에서 database 형식을 인스턴스화하고 일부 값을 사용하여 초기화했습니다. 사용할 포트 및 ServeHTTP 메서드의 사용자 지정 버전을 구현하는 db 개체를 포함하여 서버 주소를 정의한 http.ListenAndServe 함수를 사용하여 HTTP 서버를 시작했습니다. 프로그램을 실행하면 Go는 해당 메서드의 구현을 사용하며, 이것이 바로 서버 API에서 인터페이스를 사용하고 구현하는 방법이 됩니다.

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

http.Handle 함수를 사용하면 서버 API에서 인터페이스의 또 다른 사용 사례를 찾을 수 있습니다. 자세한 내용은 Go 사이트의 Writing web applications(웹 애플리케이션 작성) 게시물을 참조하세요.