본문으로 건너뛰기

인터페이스(Interface)

Go의 인터페이스는 타입이 구현해야 하는 메서드 집합을 정의합니다. 다른 언어와 달리 Go의 인터페이스는 암묵적으로(implicitly) 구현됩니다. implements 키워드 없이 인터페이스가 요구하는 메서드를 모두 구현하면 자동으로 해당 인터페이스를 만족합니다.

인터페이스 기본

인터페이스는 메서드 시그니처의 집합입니다.

package main

import (
"fmt"
"math"
)

// 인터페이스 정의
type Shape interface {
Area() float64
Perimeter() float64
}

type Circle struct {
Radius float64
}

// 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
}

type Rectangle struct {
Width, Height float64
}

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

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

// Shape 인터페이스를 매개변수로 받음
func printShapeInfo(s Shape) {
fmt.Printf("넓이: %.2f, 둘레: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Circle{Radius: 3},
}

for _, s := range shapes {
printShapeInfo(s)
}
// 넓이: 78.54, 둘레: 31.42
// 넓이: 24.00, 둘레: 20.00
// 넓이: 28.27, 둘레: 18.85
}

암묵적 구현의 장점

Go의 암묵적 인터페이스 구현은 코드를 더 유연하게 만듭니다. 인터페이스를 나중에 정의해도 기존 타입이 자동으로 구현합니다.

package main

import (
"fmt"
"strings"
)

// 나중에 정의된 인터페이스도 기존 타입이 자동 구현
type Describer interface {
Describe() string
}

type Dog struct {
Name string
Breed string
}

func (d Dog) Describe() string {
return fmt.Sprintf("%s은(는) %s 종 개입니다", d.Name, d.Breed)
}

type Car struct {
Make string
Model string
Year int
}

func (c Car) Describe() string {
return fmt.Sprintf("%d년형 %s %s", c.Year, c.Make, c.Model)
}

// 표준 라이브러리의 strings.Builder도 Write 메서드가 있으므로
// io.Writer 인터페이스를 구현
func writeAll(w interface{ Write([]byte) (int, error) }, items []string) {
for _, item := range items {
fmt.Fprintln(w.(interface{ WriteString(string) (int, error) }), item)
}
}

func printAll(items []Describer) {
for _, item := range items {
fmt.Println(item.Describe())
}
}

func main() {
items := []Describer{
Dog{Name: "바둑이", Breed: "진돗개"},
Car{Make: "현대", Model: "아반떼", Year: 2024},
Dog{Name: "콩이", Breed: "푸들"},
}
printAll(items)

// strings.Builder는 직접 정의하지 않아도 여러 인터페이스를 구현
var sb strings.Builder
fmt.Fprintln(&sb, "Hello, Go!")
fmt.Fprintln(&sb, "인터페이스는 암묵적입니다")
fmt.Print(sb.String())
}

인터페이스 중첩(Interface Embedding)

인터페이스는 다른 인터페이스를 포함할 수 있습니다.

package main

import (
"fmt"
"io"
"strings"
)

// 기본 인터페이스
type Reader interface {
Read() string
}

type Writer interface {
Write(s string)
}

// 인터페이스 중첩 — Reader와 Writer를 모두 포함
type ReadWriter interface {
Reader
Writer
}

// 추가 메서드 포함
type ReadWriteCloser interface {
ReadWriter
Close() error
}

type Buffer struct {
data strings.Builder
}

func (b *Buffer) Read() string {
return b.data.String()
}

func (b *Buffer) Write(s string) {
b.data.WriteString(s)
}

func (b *Buffer) Close() error {
b.data.Reset()
return nil
}

func useReadWriter(rw ReadWriter) {
rw.Write("Hello, ")
rw.Write("World!")
fmt.Println(rw.Read())
}

func main() {
buf := &Buffer{}
useReadWriter(buf) // "Hello, World!"

buf.Close() // 초기화

// 표준 라이브러리의 인터페이스 중첩 예시
// io.ReadWriter = io.Reader + io.Writer
var rw io.ReadWriter = strings.NewReader("test")
_ = rw
fmt.Println("표준 라이브러리도 인터페이스 중첩 활용")
}

빈 인터페이스 — any

interface{} (Go 1.18부터 any 별칭 사용 가능)는 메서드가 없으므로 모든 타입이 구현합니다.

package main

import "fmt"

// any = interface{} (Go 1.18+)
func printAnything(v any) {
fmt.Printf("값: %v, 타입: %T\n", v, v)
}

func storeAll(items ...any) []any {
return items
}

// 제네릭 컨테이너 (any 사용)
type Container struct {
items []any
}

func (c *Container) Add(item any) {
c.items = append(c.items, item)
}

func (c *Container) Get(index int) any {
if index < 0 || index >= len(c.items) {
return nil
}
return c.items[index]
}

func (c *Container) Len() int {
return len(c.items)
}

func main() {
printAnything(42) // 값: 42, 타입: int
printAnything("hello") // 값: hello, 타입: string
printAnything(3.14) // 값: 3.14, 타입: float64
printAnything([]int{1, 2}) // 값: [1 2], 타입: []int
printAnything(nil) // 값: <nil>, 타입: <nil>

items := storeAll(1, "two", 3.0, true, []byte("bytes"))
for _, item := range items {
fmt.Printf("%T: %v\n", item, item)
}

c := &Container{}
c.Add(42)
c.Add("hello")
c.Add([]float64{1.1, 2.2})

// any에서 구체 타입을 꺼내려면 타입 단언 필요 (ch7.2에서 상세 설명)
if n, ok := c.Get(0).(int); ok {
fmt.Println("정수:", n*2)
}
}

인터페이스 값의 내부 구조

인터페이스 값은 내부적으로 (타입, 값) 쌍으로 구성됩니다.

package main

import (
"fmt"
"reflect"
)

type Doer interface {
Do() string
}

type TypeA struct{ name string }
type TypeB struct{ value int }

func (a TypeA) Do() string { return "A: " + a.name }
func (b TypeB) Do() string { return fmt.Sprintf("B: %d", b.value) }

func inspectInterface(d Doer) {
fmt.Printf("값: %v\n", d)
fmt.Printf("타입: %T\n", d)
// reflect를 사용한 내부 구조 확인
v := reflect.ValueOf(d)
fmt.Printf("reflect 타입: %v\n", v.Type())
fmt.Printf("reflect 값: %v\n", v)
fmt.Println()
}

func main() {
var d Doer

fmt.Println("nil 인터페이스:", d == nil) // true

d = TypeA{name: "foo"}
inspectInterface(d)

d = TypeB{value: 42}
inspectInterface(d)

// 인터페이스 nil 주의사항
var a *TypeA = nil
d = a // (타입=*TypeA, 값=nil) — 인터페이스는 nil이 아님!
fmt.Println("포인터 nil 대입 후:", d == nil) // false!
fmt.Println("d 결과:", d.Do()) // 패닉 발생 가능

// 올바른 nil 체크
if d != nil {
// 실제 타입의 nil 여부는 타입 단언으로 확인
if a, ok := d.(*TypeA); ok && a != nil {
fmt.Println(a.Do())
}
}
}

실전 예제: 플러그인 시스템

package main

import (
"fmt"
"sort"
"strings"
)

// 플러그인 인터페이스
type Plugin interface {
Name() string
Execute(input string) string
}

// 플러그인 레지스트리
type Registry struct {
plugins map[string]Plugin
}

func NewRegistry() *Registry {
return &Registry{plugins: make(map[string]Plugin)}
}

func (r *Registry) Register(p Plugin) {
r.plugins[p.Name()] = p
}

func (r *Registry) Execute(name, input string) (string, bool) {
p, ok := r.plugins[name]
if !ok {
return "", false
}
return p.Execute(input), true
}

func (r *Registry) ListPlugins() []string {
names := make([]string, 0, len(r.plugins))
for name := range r.plugins {
names = append(names, name)
}
sort.Strings(names)
return names
}

// 플러그인 구현들
type UpperPlugin struct{}

func (p UpperPlugin) Name() string { return "upper" }
func (p UpperPlugin) Execute(s string) string { return strings.ToUpper(s) }

type ReversePlugin struct{}

func (p ReversePlugin) Name() string { return "reverse" }
func (p ReversePlugin) Execute(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}

type RepeatPlugin struct {
Times int
}

func (p RepeatPlugin) Name() string { return "repeat" }
func (p RepeatPlugin) Execute(s string) string { return strings.Repeat(s, p.Times) }

func main() {
reg := NewRegistry()
reg.Register(UpperPlugin{})
reg.Register(ReversePlugin{})
reg.Register(RepeatPlugin{Times: 3})

fmt.Println("등록된 플러그인:", reg.ListPlugins())

inputs := []struct {
plugin string
input string
}{
{"upper", "hello world"},
{"reverse", "golang"},
{"repeat", "Go! "},
{"unknown", "test"},
}

for _, tc := range inputs {
result, ok := reg.Execute(tc.plugin, tc.input)
if ok {
fmt.Printf("[%s] %q → %q\n", tc.plugin, tc.input, result)
} else {
fmt.Printf("[%s] 플러그인 없음\n", tc.plugin)
}
}
}

인터페이스 설계 원칙

package main

import "fmt"

// 나쁜 예: 너무 큰 인터페이스
type BigInterface interface {
Read() string
Write(s string)
Close() error
Flush() error
Seek(offset int) error
Len() int
// ... 메서드가 너무 많음
}

// 좋은 예: 작은 인터페이스들
type Reader interface {
Read() string
}

type Writer interface {
Write(s string)
}

type Closer interface {
Close() error
}

// 필요에 따라 조합
type ReadCloser interface {
Reader
Closer
}

// 단일 메서드 인터페이스 — 가장 강력
type Stringer interface {
String() string
}

type Logger interface {
Log(msg string)
}

// 함수 타입으로 인터페이스 구현
type LoggerFunc func(msg string)

func (f LoggerFunc) Log(msg string) {
f(msg)
}

func doWork(l Logger) {
l.Log("작업 시작")
l.Log("작업 완료")
}

func main() {
// 함수로 인터페이스 구현
doWork(LoggerFunc(func(msg string) {
fmt.Println("[LOG]", msg)
}))

// 커스텀 구조체로 구현
type FileLogger struct{ prefix string }
// 실제로는 파일에 쓰겠지만 여기선 출력
doWork(LoggerFunc(func(msg string) {
fmt.Printf("[FILE] %s\n", msg)
}))
}

핵심 정리

  • Go 인터페이스는 암묵적 구현implements 선언 불필요
  • 인터페이스 내부는 (동적 타입, 동적 값) 쌍
  • any(interface{})는 모든 타입을 담을 수 있는 빈 인터페이스
  • 인터페이스는 작게 설계 — 단일 메서드 인터페이스가 가장 유연
  • nil 인터페이스 ≠ nil 포인터를 담은 인터페이스 (주의!)
  • 인터페이스 중첩으로 더 큰 인터페이스 구성 가능