본문으로 건너뛰기
Go 인터페이스와 다형성 완벽 가이드

# Go의 인터페이스와 다형성: 유연한 코드 작성하기

Table of Contents

목차

  1. 인터페이스 기본 개념
  2. 인터페이스 정의와 구현
  3. 다형성 (Polymorphism)
  4. 빈 인터페이스 (Empty Interface)
  5. 타입 단언과 타입 스위치
  6. 인터페이스 조합
  7. 실전 인터페이스 패턴
  8. 표준 라이브러리 인터페이스
  9. 인터페이스 디자인 원칙
  10. 일반적인 실수와 해결책

인터페이스 기본 개념

Go의 인터페이스는 메서드 시그니처의 집합입니다. 타입이 인터페이스의 모든 메서드를 구현하면 자동으로 그 인터페이스를 만족합니다. Java나 C#처럼 명시적으로 implements 키워드를 쓸 필요가 없습니다. 이것이 Go 인터페이스의 가장 큰 매력입니다.

:::tip Go 인터페이스의 특징

  • 암시적 구현: implements 키워드 불필요. 그냥 메서드만 구현하면 됩니다.
  • 작은 인터페이스: 단일 메서드 인터페이스를 선호합니다 (예: io.Reader).
  • 덕 타이핑: “오리처럼 걷고 꽥꽥거리면 오리다”. 타입이 무엇인지보다 무엇을 할 수 있는지가 중요합니다.
  • 유연성: 기존 타입도 새로운 인터페이스를 만족할 수 있습니다. :::

기본 인터페이스 정의

package main

import "fmt"

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

// Circle 타입
type Circle struct {
    Radius float64
}

// Circle이 Shape 인터페이스를 구현
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

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

// Rectangle 타입
type Rectangle struct {
    Width, Height float64
}

// Rectangle이 Shape 인터페이스를 구현
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

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

func printShapeInfo(s Shape) {
    fmt.Printf("면적: %.2f\n", s.Area())
    fmt.Printf("둘레: %.2f\n", s.Perimeter())
}

func main() {
    c := Circle{Radius: 5}
    r := Rectangle{Width: 4, Height: 6}

    printShapeInfo(c)
    fmt.Println()
    printShapeInfo(r)
}

인터페이스 값의 내부 구조

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct {
    Name string
}

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

func main() {
    var s Speaker

    // nil 인터페이스
    fmt.Printf("Type: %T, Value: %v, Is nil: %v\n", s, s, s == nil)

    // 인터페이스에 값 할당
    s = Dog{Name: "바둑이"}
    fmt.Printf("Type: %T, Value: %v, Is nil: %v\n", s, s, s == nil)

    // 인터페이스 메서드 호출
    fmt.Println(s.Speak())
}

인터페이스 정의와 구현

단일 메서드 인터페이스

package main

import (
    "fmt"
    "io"
)

// 읽기 인터페이스 (io.Reader와 유사)
type Reader interface {
    Read(p []byte) (n int, err error)
}

// 쓰기 인터페이스 (io.Writer와 유사)
type Writer interface {
    Write(p []byte) (n int, err error)
}

// 닫기 인터페이스 (io.Closer와 유사)
type Closer interface {
    Close() error
}

// 커스텀 Reader 구현
type StringReader struct {
    data string
    pos  int
}

func (sr *StringReader) Read(p []byte) (n int, err error) {
    if sr.pos >= len(sr.data) {
        return 0, io.EOF
    }

    n = copy(p, sr.data[sr.pos:])
    sr.pos += n
    return n, nil
}

func main() {
    sr := &StringReader{data: "Hello, Go!"}
    buf := make([]byte, 5)

    for {
        n, err := sr.Read(buf)
        if err == io.EOF {
            break
        }
        fmt.Printf("Read %d bytes: %s\n", n, buf[:n])
    }
}

다중 메서드 인터페이스

package main

import "fmt"

// Database 인터페이스
type Database interface {
    Connect() error
    Disconnect() error
    Query(sql string) ([]map[string]interface{}, error)
    Execute(sql string) error
}

// MySQL 구현
type MySQL struct {
    host     string
    port     int
    username string
    password string
    connected bool
}

func (m *MySQL) Connect() error {
    fmt.Printf("MySQL에 연결: %s:%d\n", m.host, m.port)
    m.connected = true
    return nil
}

func (m *MySQL) Disconnect() error {
    fmt.Println("MySQL 연결 해제")
    m.connected = false
    return nil
}

func (m *MySQL) Query(sql string) ([]map[string]interface{}, error) {
    fmt.Printf("쿼리 실행: %s\n", sql)
    return []map[string]interface{}ె
        {"id": 1, "name": "홍길동"},
        {"id": 2, "name": "김철수"},
    }, nil
}

func (m *MySQL) Execute(sql string) error {
    fmt.Printf("SQL 실행: %s\n", sql)
    return nil
}

// PostgreSQL 구현
type PostgreSQL struct {
    connectionString string
    connected        bool
}

func (p *PostgreSQL) Connect() error {
    fmt.Printf("PostgreSQL에 연결: %s\n", p.connectionString)
    p.connected = true
    return nil
}

func (p *PostgreSQL) Disconnect() error {
    fmt.Println("PostgreSQL 연결 해제")
    p.connected = false
    return nil
}

func (p *PostgreSQL) Query(sql string) ([]map[string]interface{}, error) {
    fmt.Printf("쿼리 실행: %s\n", sql)
    return []map[string]interface{}ె
        {"id": 1, "name": "이영희"},
    }, nil
}

func (p *PostgreSQL) Execute(sql string) error {
    fmt.Printf("SQL 실행: %s\n", sql)
    return nil
}

func performDatabaseOperations(db Database) {
    db.Connect()
    defer db.Disconnect()

    results, _ := db.Query("SELECT * FROM users")
    fmt.Printf("결과: %v\n", results)

    db.Execute("UPDATE users SET active = true")
}

func main() {
    mysql := &MySQL{
        host:     "localhost",
        port:     3306,
        username: "root",
        password: "password",
    }

    postgres := &PostgreSQL{
        connectionString: "host=localhost port=5432 user=admin",
    }

    fmt.Println("=== MySQL ===")
    performDatabaseOperations(mysql)

    fmt.Println("\n=== PostgreSQL ===")
    performDatabaseOperations(postgres)
}

다형성 (Polymorphism)

다형성은 하나의 인터페이스로 여러 타입을 다룰 수 있는 능력입니다. Go에서는 인터페이스를 통해 이를 아주 자연스럽게 구현할 수 있습니다.

기본 다형성 예제

package main

import "fmt"

// Animal 인터페이스
type Animal interface {
    Speak() string
    Move() string
}

// Dog 구현
type Dog struct {
    Name string
}

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

func (d Dog) Move() string {
    return "네 발로 걷기"
}

// Cat 구현
type Cat struct {
    Name string
}

func (c Cat) Speak() string {
    return "야옹~"
}

func (c Cat) Move() string {
    return "살금살금 걷기"
}

// Bird 구현
type Bird struct {
    Name string
}

func (b Bird) Speak() string {
    return "짹짹!"
}

func (b Bird) Move() string {
    return "날기"
}

func describe(a Animal) {
    fmt.Printf("소리: %s\n", a.Speak())
    fmt.Printf("이동: %s\n", a.Move())
}

func main() {
    animals := []Animal{
        Dog{Name: "바둑이"},
        Cat{Name: "나비"},
        Bird{Name: "짹짹이"},
    }

    for i, animal := range animals {
        fmt.Printf("\n=== 동물 %d ===\n", i+1)
        describe(animal)
    }
}

전략 패턴 (Strategy Pattern)

package main

import "fmt"

// SortStrategy 인터페이스
type SortStrategy interface {
    Sort([]int) []int
}

// BubbleSort 구현
type BubbleSort struct{}

func (b BubbleSort) Sort(data []int) []int {
    n := len(data)
    sorted := make([]int, n)
    copy(sorted, data)

    for i := 0; i < n-1; i++ {
        for j := 0; j < n-i-1; j++ {
            if sorted[j] > sorted[j+1] {
                sorted[j], sorted[j+1] = sorted[j+1], sorted[j]
            }
        }
    }
    return sorted
}

// QuickSort 구현
type QuickSort struct{}

func (q QuickSort) Sort(data []int) []int {
    if len(data) < 2 {
        return data
    }

    sorted := make([]int, len(data))
    copy(sorted, data)

    pivot := sorted[0]
    var left, right []int

    for _, v := range sorted[1:] {
        if v <= pivot {
            left = append(left, v)
        } else {
            right = append(right, v)
        }
    }

    result := append(q.Sort(left), pivot)
    result = append(result, q.Sort(right)...)
    return result
}

// Context
type Sorter struct {
    strategy SortStrategy
}

func (s *Sorter) SetStrategy(strategy SortStrategy) {
    s.strategy = strategy
}

func (s *Sorter) Sort(data []int) []int {
    return s.strategy.Sort(data)
}

func main() {
    data := []int{64, 34, 25, 12, 22, 11, 90}

    sorter := &Sorter{}

    // BubbleSort 전략 사용
    sorter.SetStrategy(BubbleSort{})
    fmt.Println("BubbleSort:", sorter.Sort(data))

    // QuickSort 전략 사용
    sorter.SetStrategy(QuickSort{})
    fmt.Println("QuickSort:", sorter.Sort(data))
}

빈 인터페이스 (Empty Interface)

interface{}는 모든 타입을 담을 수 있는 특별한 인터페이스입니다. Go 1.18+에서는 any라는 별칭으로 더 편하게 사용할 수 있습니다.

기본 사용

package main

import "fmt"

func printAnything(v interface{}) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

func main() {
    printAnything(42)
    printAnything("hello")
    printAnything(3.14)
    printAnything(true)
    printAnything([]int{1, 2, 3})
    printAnything(map[string]int{"age": 30})
}

제네릭 컬렉션

package main

import "fmt"

// Go 1.18+ 제네릭 사용
type Container[T any] struct {
    items []T
}

func (c *Container[T]) Add(item T) {
    c.items = append(c.items, item)
}

func (c *Container[T]) Get(index int) T {
    return c.items[index]
}

func (c *Container[T]) Size() int {
    return len(c.items)
}

// Go 1.18 이전: interface{} 사용
type OldContainer struct {
    items []interface{}
}

func (c *OldContainer) Add(item interface{}) {
    c.items = append(c.items, item)
}

func (c *OldContainer) Get(index int) interface{} {
    return c.items[index]
}

func main() {
    // 제네릭 버전
    intContainer := &Container[int]{}
    intContainer.Add(1)
    intContainer.Add(2)
    intContainer.Add(3)
    fmt.Println("제네릭:", intContainer.Get(0))

    stringContainer := &Container[string]{}
    stringContainer.Add("hello")
    stringContainer.Add("world")
    fmt.Println("제네릭:", stringContainer.Get(0))

    // 구버전
    oldContainer := &OldContainer{}
    oldContainer.Add(42)
    oldContainer.Add("hello")

    // 타입 단언 필요
    if val, ok := oldContainer.Get(0).(int); ok {
        fmt.Println("Old container:", val)
    }
}

타입 단언과 타입 스위치

타입 단언 (Type Assertion)

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    // 기본 타입 단언
    s := i.(string)
    fmt.Println(s)

    // 안전한 타입 단언
    s, ok := i.(string)
    if ok {
        fmt.Println("문자열:", s)
    }

    // 잘못된 타입 단언 (panic 발생)
    // n := i.(int) // panic!

    // 안전하게 처리
    n, ok := i.(int)
    if !ok {
        fmt.Println("정수가 아닙니다")
    } else {
        fmt.Println(n)
    }
}

타입 스위치 (Type Switch)

package main

import "fmt"

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("정수: %d\n", v)
    case string:
        fmt.Printf("문자열: %s (길이: %d)\n", v, len(v))
    case bool:
        fmt.Printf("불린: %t\n", v)
    case []int:
        fmt.Printf("정수 슬라이스: %v (길이: %d)\n", v, len(v))
    case map[string]int:
        fmt.Printf("맵: %v (크기: %d)\n", v, len(v))
    case func(int) int:
        fmt.Println("정수를 받아 정수를 반환하는 함수")
    case nil:
        fmt.Println("nil 값")
    default:
        fmt.Printf("알 수 없는 타입: %T\n", v)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe(true)
    describe([]int{1, 2, 3})
    describe(map[string]int{"age": 30})
    describe(func(x int) int { return x * 2 })
    describe(nil)

    type Person struct {
        Name string
    }
    describe(Person{Name: "홍길동"})
}

인터페이스 구현 확인

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
    return "멍멍"
}

type Cat struct{}

func (c Cat) Speak() string {
    return "야옹"
}

type Rock struct{}

func checkSpeaker(v interface{}) {
    if speaker, ok := v.(Speaker); ok {
        fmt.Printf("%T는 Speaker 인터페이스를 구현: %s\n",
            v, speaker.Speak())
    } else {
        fmt.Printf("%T는 Speaker 인터페이스를 구현하지 않음\n", v)
    }
}

func main() {
    checkSpeaker(Dog{})
    checkSpeaker(Cat{})
    checkSpeaker(Rock{})
    checkSpeaker(42)
}

인터페이스 조합

Go는 인터페이스 임베딩을 통해 작은 인터페이스를 조합할 수 있습니다.

기본 인터페이스 조합

package main

import "fmt"

// 작은 인터페이스들
type Reader interface {
    Read(p []byte) (n int, err error)
}

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

type Closer interface {
    Close() error
}

// 인터페이스 조합
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

type File struct {
    name   string
    data   []byte
    pos    int
    closed bool
}

func (f *File) Read(p []byte) (n int, err error) {
    if f.closed {
        return 0, fmt.Errorf("file is closed")
    }
    n = copy(p, f.data[f.pos:])
    f.pos += n
    return n, nil
}

func (f *File) Write(p []byte) (n int, err error) {
    if f.closed {
        return 0, fmt.Errorf("file is closed")
    }
    f.data = append(f.data, p...)
    return len(p), nil
}

func (f *File) Close() error {
    f.closed = true
    fmt.Printf("파일 %s 닫힘\n", f.name)
    return nil
}

func useReadWriter(rw ReadWriter) {
    data := []byte("Hello, ")
    rw.Write(data)

    buf := make([]byte, 10)
    n, _ := rw.Read(buf)
    fmt.Printf("읽음: %s\n", buf[:n])
}

func useReadWriteCloser(rwc ReadWriteCloser) {
    defer rwc.Close()

    rwc.Write([]byte("데이터"))
    buf := make([]byte, 10)
    n, _ := rwc.Read(buf)
    fmt.Printf("읽음: %s\n", buf[:n])
}

func main() {
    file := &File{name: "test.txt", data: make([]byte, 0)}

    useReadWriter(file)
    file.pos = 0 // 리셋

    useReadWriteCloser(file)
}

표준 라이브러리 인터페이스 조합

package main

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

// io.ReadCloser 예제
func processReadCloser(rc io.ReadCloser) error {
    defer rc.Close()

    buf := make([]byte, 100)
    n, err := rc.Read(buf)
    if err != nil && err != io.EOF {
        return err
    }

    fmt.Printf("읽은 데이터: %s\n", buf[:n])
    return nil
}

// io.WriteCloser 예제
func processWriteCloser(wc io.WriteCloser) error {
    defer wc.Close()

    _, err := wc.Write([]byte("Hello, World!"))
    return err
}

func main() {
    // strings.Reader는 io.Reader를 구현
    reader := strings.NewReader("Go 인터페이스는 강력합니다!")

    // io.NopCloser는 Reader를 ReadCloser로 변환
    rc := io.NopCloser(reader)

    processReadCloser(rc)
}

실전 인터페이스 패턴

1. 의존성 주입 (Dependency Injection)

package main

import (
    "fmt"
    "time"
)

// 인터페이스 정의
type Logger interface {
    Log(message string)
}

type UserRepository interface {
    Save(user User) error
    FindByID(id int) (*User, error)
}

type User struct {
    ID   int
    Name string
}

// Logger 구현
type ConsoleLogger struct{}

func (l ConsoleLogger) Log(message string) {
    fmt.Printf("[%s] %s\n", time.Now().Format("15:04:05"), message)
}

type FileLogger struct {
    filename string
}

func (l FileLogger) Log(message string) {
    fmt.Printf("[FILE: %s] %s\n", l.filename, message)
}

// Repository 구현
type MemoryUserRepository struct {
    users map[int]*User
}

func NewMemoryUserRepository() *MemoryUserRepository {
    return &MemoryUserRepository{
        users: make(map[int]*User),
    }
}

func (r *MemoryUserRepository) Save(user User) error {
    r.users[user.ID] = &user
    return nil
}

func (r *MemoryUserRepository) FindByID(id int) (*User, error) {
    user, exists := r.users[id]
    if !exists {
        return nil, fmt.Errorf("user not found")
    }
    return user, nil
}

// 서비스 - 인터페이스에 의존
type UserService struct {
    logger     Logger
    repository UserRepository
}

func NewUserService(logger Logger, repo UserRepository) *UserService {
    return &UserService{
        logger:     logger,
        repository: repo,
    }
}

func (s *UserService) CreateUser(name string) (*User, error) {
    s.logger.Log(fmt.Sprintf("사용자 생성 중: %s", name))

    user := User{
        ID:   int(time.Now().Unix()),
        Name: name,
    }

    err := s.repository.Save(user)
    if err != nil {
        s.logger.Log(fmt.Sprintf("사용자 생성 실패: %v", err))
        return nil, err
    }

    s.logger.Log(fmt.Sprintf("사용자 생성 완료: ID=%d", user.ID))
    return &user, nil
}

func main() {
    // Console Logger 사용
    consoleLogger := ConsoleLogger{}
    repo := NewMemoryUserRepository()
    service := NewUserService(consoleLogger, repo)

    user, _ := service.CreateUser("홍길동")
    fmt.Printf("생성된 사용자: %+v\n\n", user)

    // File Logger로 교체
    fileLogger := FileLogger{filename: "users.log"}
    service2 := NewUserService(fileLogger, repo)
    service2.CreateUser("김철수")
}

2. 어댑터 패턴 (Adapter Pattern)

package main

import "fmt"

// 기존 인터페이스
type MediaPlayer interface {
    Play(audioType string, filename string)
}

// 새로운 인터페이스
type AdvancedMediaPlayer interface {
    PlayVLC(filename string)
    PlayMP4(filename string)
}

// AdvancedMediaPlayer 구현
type VLCPlayer struct{}

func (v VLCPlayer) PlayVLC(filename string) {
    fmt.Printf("VLC로 재생 중: %s\n", filename)
}

func (v VLCPlayer) PlayMP4(filename string) {
    // VLC는 MP4를 지원하지 않음
}

type MP4Player struct{}

func (m MP4Player) PlayVLC(filename string) {
    // MP4는 VLC를 지원하지 않음
}

func (m MP4Player) PlayMP4(filename string) {
    fmt.Printf("MP4로 재생 중: %s\n", filename)
}

// 어댑터
type MediaAdapter struct {
    advancedPlayer AdvancedMediaPlayer
}

func NewMediaAdapter(audioType string) *MediaAdapter {
    if audioType == "vlc" {
        return &MediaAdapter{advancedPlayer: VLCPlayer{}}
    } else if audioType == "mp4" {
        return &MediaAdapter{advancedPlayer: MP4Player{}}
    }
    return nil
}

func (m *MediaAdapter) Play(audioType string, filename string) {
    if audioType == "vlc" {
        m.advancedPlayer.PlayVLC(filename)
    } else if audioType == "mp4" {
        m.advancedPlayer.PlayMP4(filename)
    }
}

// 기본 플레이어
type AudioPlayer struct {
    adapter *MediaAdapter
}

func (a *AudioPlayer) Play(audioType string, filename string) {
    if audioType == "mp3" {
        fmt.Printf("MP3 재생 중: %s\n", filename)
    } else if audioType == "vlc" || audioType == "mp4" {
        a.adapter = NewMediaAdapter(audioType)
        a.adapter.Play(audioType, filename)
    } else {
        fmt.Printf("지원하지 않는 형식: %s\n", audioType)
    }
}

func main() {
    player := &AudioPlayer{}

    player.Play("mp3", "song.mp3")
    player.Play("vlc", "movie.vlc")
    player.Play("mp4", "video.mp4")
    player.Play("avi", "clip.avi")
}

3. 데코레이터 패턴 (Decorator Pattern)

package main

import (
    "fmt"
    "strings"
    "time"
)

// 기본 인터페이스
type Component interface {
    Operation() string
}

// 기본 구현
type ConcreteComponent struct {
    data string
}

func (c *ConcreteComponent) Operation() string {
    return c.data
}

// 데코레이터 베이스
type Decorator struct {
    component Component
}

func (d *Decorator) Operation() string {
    return d.component.Operation()
}

// 구체적 데코레이터: 대문자 변환
type UpperCaseDecorator struct {
    Decorator
}

func (u *UpperCaseDecorator) Operation() string {
    return strings.ToUpper(u.component.Operation())
}

// 구체적 데코레이터: 타임스탬프 추가
type TimestampDecorator struct {
    Decorator
}

func (t *TimestampDecorator) Operation() string {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    return fmt.Sprintf("[%s] %s", timestamp, t.component.Operation())
}

// 구체적 데코레이터: 테두리 추가
type BorderDecorator struct {
    Decorator
}

func (b *BorderDecorator) Operation() string {
    result := b.component.Operation()
    border := strings.Repeat("=", len(result)+4)
    return fmt.Sprintf("%s\n| %s |\n%s", border, result, border)
}

func main() {
    // 기본 컴포넌트
    component := &ConcreteComponent{data: "hello world"}
    fmt.Println("기본:", component.Operation())
    fmt.Println()

    // 대문자 데코레이터
    upper := &UpperCaseDecorator{
        Decorator: Decorator{component: component},
    }
    fmt.Println("대문자:", upper.Operation())
    fmt.Println()

    // 타임스탬프 + 대문자
    timestamp := &TimestampDecorator{
        Decorator: Decorator{component: upper},
    }
    fmt.Println("타임스탬프 + 대문자:", timestamp.Operation())
    fmt.Println()

    // 테두리 + 타임스탬프 + 대문자
    border := &BorderDecorator{
        Decorator: Decorator{component: timestamp},
    }
    fmt.Println("모든 데코레이터:")
    fmt.Println(border.Operation())
}

표준 라이브러리 인터페이스

fmt.Stringer

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d세)", p.Name, p.Age)
}

type Point struct {
    X, Y int
}

func (p Point) String() string {
    return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

func main() {
    person := Person{Name: "홍길동", Age: 30}
    point := Point{X: 10, Y: 20}

    fmt.Println(person) // String() 메서드 자동 호출
    fmt.Println(point)

    fmt.Printf("사람: %v\n", person)
    fmt.Printf("좌표: %s\n", point)
}

error 인터페이스

package main

import "fmt"

// 커스텀 에러 타입
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on '%s': %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "must be non-negative",
        }
    }
    if age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "must be less than 150",
        }
    }
    return nil
}

func main() {
    ages := []int{25, -5, 200, 30}

    for _, age := range ages {
        err := validateAge(age)
        if err != nil {
            fmt.Printf("에러: %v\n", err)

            // 타입 단언으로 상세 정보 접근
            if ve, ok := err.(*ValidationError); ok {
                fmt.Printf(" 필드: %s\n", ve.Field)
                fmt.Printf(" 메시지: %s\n", ve.Message)
            }
        } else {
            fmt.Printf("나이 %d는 유효함\n", age)
        }
    }
}

sort.Interface

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

type ByName []Person

func (n ByName) Len() int           { return len(n) }
func (n ByName) Less(i, j int) bool { return n[i].Name < n[j].Name }
func (n ByName) Swap(i, j int)      { n[i], n[j] = n[j], n[i] }

func main() {
    people := []Person{
        {"홍길동", 30},
        {"김철수", 25},
        {"이영희", 35},
        {"박민수", 28},
    }

    fmt.Println("원본:", people)

    // 나이로 정렬
    sort.Sort(ByAge(people))
    fmt.Println("나이순:", people)

    // 이름으로 정렬
    sort.Sort(ByName(people))
    fmt.Println("이름순:", people)

    // 역순 정렬
    sort.Sort(sort.Reverse(ByAge(people)))
    fmt.Println("나이 역순:", people)
}

인터페이스 디자인 원칙

1. 작은 인터페이스 선호

// 좋은 예: 작고 집중된 인터페이스
type Reader interface {
    Read(p []byte) (n int, err error)
}

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

type Closer interface {
    Close() error
}

// 나쁜 예: 너무 큰 인터페이스
type BadInterface interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Close() error
    Open(name string) error
    Sync() error
    Truncate(size int64) error
    // ... 더 많은 메서드
}

:::tip 인터페이스 디자인 원칙

  1. 작게 유지: 1-3개의 메서드가 이상적
  2. 명확한 목적: 인터페이스는 하나의 책임만
  3. 소비자 중심: 인터페이스는 사용하는 쪽에서 정의
  4. 조합 가능: 작은 인터페이스를 조합하여 큰 인터페이스 생성 :::

2. 인터페이스 분리 원칙 (ISP)

package main

import "fmt"

// 나쁜 예: 모든 기능이 하나의 인터페이스에
type BadWorker interface {
    Work()
    Eat()
    Sleep()
}

// 좋은 예: 기능별로 분리
type Worker interface {
    Work()
}

type Eater interface {
    Eat()
}

type Sleeper interface {
    Sleep()
}

// 필요한 인터페이스만 조합
type Human interface {
    Worker
    Eater
    Sleeper
}

type Robot struct {
    Name string
}

func (r Robot) Work() {
    fmt.Printf("%s: 작업 중...\n", r.Name)
}

// Robot은 Eat, Sleep을 구현하지 않음 - 필요 없으므로

type Employee struct {
    Name string
}

func (e Employee) Work() {
    fmt.Printf("%s: 일하는 중...\n", e.Name)
}

func (e Employee) Eat() {
    fmt.Printf("%s: 식사 중...\n", e.Name)
}

func (e Employee) Sleep() {
    fmt.Printf("%s: 수면 중...\n", e.Name)
}

func doWork(w Worker) {
    w.Work()
}

func main() {
    robot := Robot{Name: "R2D2"}
    employee := Employee{Name: "홍길동"}

    doWork(robot)
    doWork(employee)
}

일반적인 실수와 해결책

실수 1: nil 인터페이스 vs nil 값

package main

import "fmt"

type MyError struct {
    msg string
}

func (e *MyError) Error() string {
    return e.msg
}

// 잘못된 예
func badFunction() error {
    var err *MyError // nil pointer
    // ... 일부 로직
    return err // nil pointer지만 interface는 nil이 아님!
}

// 올바른 예
func goodFunction() error {
    var err *MyError
    if err != nil {
        return err
    }
    return nil // 명시적으로 nil 반환
}

func main() {
    err1 := badFunction()
    fmt.Printf("badFunction: err == nil? %v (type: %T)\n",
        err1 == nil, err1)

    err2 := goodFunction()
    fmt.Printf("goodFunction: err == nil? %v\n", err2 == nil)
}

:::warning nil 인터페이스 주의 인터페이스가 nil이 되려면 타입과 값 모두 nil이어야 합니다. nil 포인터를 가진 인터페이스는 nil이 아닙니다! :::

실수 2: 인터페이스 구현 확인 누락

package main

import "fmt"

type Speaker interface {
    Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
    return "멍멍"
}

type Cat struct{}

// Cat은 Speak()을 구현하지 않음 - 컴파일 에러는 발생하지 않음

func main() {
    // 컴파일 타임에 인터페이스 구현 확인
    var _ Speaker = Dog{} // OK

    // var _ Speaker = Cat{} // 컴파일 에러!
    // Cat does not implement Speaker (missing Speak method)
}

실수 3: 과도한 인터페이스 사용

// 나쁜 예: 불필요한 인터페이스
type UserGetter interface {
    GetUser() User
}

type SimpleService struct {
    user User
}

func (s SimpleService) GetUser() User {
    return s.user
}

// 이 경우 인터페이스가 필요 없음 - 구체 타입 사용이 더 간단

// 좋은 예: 인터페이스가 실제로 필요한 경우
type UserService interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
    DeleteUser(id int) error
}

type User struct {
    ID   int
    Name string
}

// 여러 구현이 필요할 때만 인터페이스 사용

테스팅

인터페이스를 활용한 테스트

package main

import (
    "errors"
    "testing"
)

// 인터페이스 정의
type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

type User struct {
    ID   int
    Name string
}

// 프로덕션 구현
type DatabaseUserRepository struct {
    // 실제 데이터베이스 연결
}

func (r *DatabaseUserRepository) FindByID(id int) (*User, error) {
    // 실제 데이터베이스 쿼리
    return nil, nil
}

func (r *DatabaseUserRepository) Save(user *User) error {
    // 실제 데이터베이스 저장
    return nil
}

// 테스트용 Mock 구현
type MockUserRepository struct {
    users map[int]*User
}

func NewMockUserRepository() *MockUserRepository {
    return &MockUserRepository{
        users: make(map[int]*User),
    }
}

func (m *MockUserRepository) FindByID(id int) (*User, error) {
    user, exists := m.users[id]
    if !exists {
        return nil, errors.New("user not found")
    }
    return user, nil
}

func (m *MockUserRepository) Save(user *User) error {
    m.users[user.ID] = user
    return nil
}

// 테스트할 서비스
type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUserName(id int) (string, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return "", err
    }
    return user.Name, nil
}

// 테스트
func TestUserService_GetUserName(t *testing.T) {
    // Mock 리포지토리 사용
    mockRepo := NewMockUserRepository()
    mockRepo.Save(&User{ID: 1, Name: "홍길동"})

    service := &UserService{repo: mockRepo}

    name, err := service.GetUserName(1)
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }

    if name != "홍길동" {
        t.Errorf("expected '홍길동', got '%s'", name)
    }

    // 존재하지 않는 사용자
    _, err = service.GetUserName(999)
    if err == nil {
        t.Error("expected error for non-existent user")
    }
}

요약 및 모범 사례

:::tip Go 인터페이스 모범 사례

  1. 작게 유지: 1-3개 메서드가 이상적
  2. 암시적 구현: implements 키워드 불필요
  3. 소비자가 정의: 인터페이스는 사용하는 쪽에서 정의
  4. 조합 활용: 작은 인터페이스를 조합
  5. 표준 인터페이스 사용: io.Reader, error 등
  6. 테스트 용이성: 인터페이스로 테스트 가능하게
  7. nil 주의: 인터페이스의 nil 체크 주의
  8. 필요할 때만: 불필요한 추상화 피하기

:::

go run interfaces_example.go

# 테스트 실행
go test -v

# 인터페이스 구현 확인
go build # 컴파일 타임에 체크

참고 자료

이 글 공유하기:
My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트