Go의 비밀스러운 삶: 인터페이스

발행: (2025년 12월 2일 오후 02:46 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

암묵적 계약의 힘

화요일 아침 안개가 끼었다. 이선은 커피와 작은 비스코티 상자를 들고 보관소로 내려갔다.
엘리노어가 고개를 들었다. “오늘은 이탈리안인가?”
“제빵사가 비스코티는 커피잔에 딱 맞게 설계됐다고 했어—형태는 기능을 따른다.”
그녀가 미소 지었다. “완벽해. 오늘은 인터페이스에 대해 이야기할 거야—Go가 어떤 것이든 그것이 할 수 있는 일을 정의하는 방식이야.”

간단한 예제

package main

import "fmt"

type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow!"
}

func main() {
    dog := Dog{Name: "Buddy"}
    cat := Cat{Name: "Whiskers"}

    fmt.Println(dog.Name, "says:", dog.Speak())
    fmt.Println(cat.Name, "says:", cat.Speak())
}

Output

Buddy says: Woof!
Whiskers says: Meow!

두 개의 서로 다른 타입—DogCat. 두 타입 모두 문자열을 반환하는 Speak() 메서드를 가지고 있지만, 서로 관련 없는 타입이다.

인터페이스 정의하기

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "Meow!"
}

func MakeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "Buddy"}
    cat := Cat{Name: "Whiskers"}

    MakeItSpeak(dog)
    MakeItSpeak(cat)
}

Output

Woof!
Meow!

Speaker는 계약을 정의한다: Speak() string 메서드를 가진 모든 타입은 인터페이스를 만족한다. MakeItSpeak 함수는 어떤 Speaker든 받아들이며, DogCat 모두 해당한다.

Note: Go에서는 타입이 인터페이스를 구현한다는 것을 명시적으로 선언하지 않는다. 메서드가 일치하면 구현은 암묵적이다.

왜 암묵적인가?

  • 유연성: 이미 존재하는 타입이 인터페이스를 만족하도록 정의할 수 있다. 그 타입들이 인터페이스가 만들어지기 전에 작성되었더라도 가능하다.
  • 사전 결합 없음: 타입이 관계를 선언하도록 강요되지 않으므로 코드가 느슨하게 결합된다.

실제 예시: 다양한 대상에 쓰기

package main

import (
    "fmt"
    "os"
)

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

type ConsoleWriter struct{}

func (cw ConsoleWriter) Write(data []byte) (int, error) {
    n, err := fmt.Println(string(data))
    return n, err
}

func WriteMessage(w Writer, message string) {
    w.Write([]byte(message))
}

func main() {
    console := ConsoleWriter{}
    WriteMessage(console, "Hello, Console!")

    file, _ := os.Create("output.txt")
    defer file.Close()
    WriteMessage(file, "Hello, File!")
}

Writer 인터페이스는 단일 메서드 Write([]byte) (int, error)를 가진다.

  • ConsoleWriter가 이를 구현한다.
  • 표준 라이브러리의 os.File도 이 인터페이스를 구현한다. 이 파일은 우리 인터페이스가 존재하기 훨씬 전에 작성되었다.

따라서 WriteMessageWriter를 만족하는 어떤 타입—콘솔, 파일, 네트워크 연결, 메모리 버퍼 등—과도 동작한다.

인터페이스를 이용한 다형성

package main

import "fmt"

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

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * 3.14159 * c.Radius
}

func PrintShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 5, Height: 3}
    circle := Circle{Radius: 4}

    PrintShapeInfo(rect)
    PrintShapeInfo(circle)
}

Output

Area: 15.00, Perimeter: 16.00
Area: 50.27, Perimeter: 25.13

ShapeArea()Perimeter()를 요구한다. RectangleCircle 모두 이를 만족하므로 PrintShapeInfo는 구체적인 타입을 알 필요 없이 모든 Shape에 대해 동작한다.

이는 덕 타이핑과 비슷하지만, Go는 컴파일 시점에 구현을 검사해 타입 안전성을 제공한다.

빈 인터페이스 (any)

package main

import "fmt"

func PrintAnything(v any) {
    fmt.Println(v)
}

func main() {
    PrintAnything(42)
    PrintAnything("hello")
    PrintAnything(true)
    PrintAnything([]int{1, 2, 3})
}

Output

42
hello
true
[1 2 3]

any는 빈 인터페이스 interface{}의 별칭이다. 모든 타입은 최소한 메서드가 0개이므로 모두 이를 만족한다. 컴파일 타임 타입 안전성을 없애므로 남용하지 말아야 한다.

타입 어설션

package main

import "fmt"

func Describe(v any) {
    // 콤마‑ok 형태의 타입 어설션
    if str, ok := v.(string); ok {
        fmt.Printf("String: %s (length %d)\n", str, len(str))
        return
    }

    if num, ok := v.(int); ok {
        fmt.Printf("Integer: %d (doubled: %d)\n", num, num*2)
        return
    }

    fmt.Printf("Unknown type: %T\n", v)
}

func main() {
    Describe("hello")
    Describe(42)
    Describe(true)
}

Output

String: hello (length 5)
Integer: 42 (doubled: 84)
Unknown type: bool

ok 검사를 생략하고 str := v.(string)처럼 쓰면 어설션이 실패했을 때 프로그램이 패닉한다.

타입 스위치

package main

import "fmt"

func Describe(v any) {
    switch val := v.(type) {
    case string:
        fmt.Printf("String: %s (length %d)\n", val, len(val))
    case int:
        fmt.Printf("Integer: %d (doubled: %d)\n", val, val*2)
    case bool:
        fmt.Printf("Boolean: %t\n", val)
    default:
        fmt.Printf("Unknown type: %T\n", val)
    }
}

func main() {
    Describe("hello")
    Describe(42)
    Describe(true)
    Describe(3.14)
}

Output

String: hello (length 5)
Integer: 42 (doubled: 84)
Boolean: true
Unknown type: float64

switch val := v.(type) 구문은 여러 가능한 구체 타입을 깔끔하게 처리한다.

인터페이스 계층 구조 (구체적 → 일반)

  • 구체적 인터페이스

    type Reader interface {
        Read([]byte) (int, error)
    }

    Read([]byte) (int, error)를 구현한 타입만 Reader를 만족한다.

  • 빈 인터페이스 (any)

    type any interface{}

    모든 타입이 이를 만족하므로 가장 일반적인 인터페이스다. 정말 모든 타입을 받아야 할 때만 사용하라.

Back to Blog

관련 글

더 보기 »