Go의 인터페이스와 다형성: 유연한 코드 작성하기
목차
- 인터페이스 기본 개념
- 인터페이스 정의와 구현
- 다형성 (Polymorphism)
- 빈 인터페이스 (Empty Interface)
- 타입 단언과 타입 스위치
- 인터페이스 조합
- 실전 인터페이스 패턴
- 표준 라이브러리 인터페이스
- 인터페이스 디자인 원칙
- 일반적인 실수와 해결책
인터페이스 기본 개념
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}
// Contexttype 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-3개의 메서드가 이상적
- 명확한 목적: 인터페이스는 하나의 책임만
- 소비자 중심: 인터페이스는 사용하는 쪽에서 정의
- 조합 가능: 작은 인터페이스를 조합하여 큰 인터페이스 생성 :::
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-3개 메서드가 이상적
- 암시적 구현: implements 키워드 불필요
- 소비자가 정의: 인터페이스는 사용하는 쪽에서 정의
- 조합 활용: 작은 인터페이스를 조합
- 표준 인터페이스 사용: io.Reader, error 등
- 테스트 용이성: 인터페이스로 테스트 가능하게
- nil 주의: 인터페이스의 nil 체크 주의
- 필요할 때만: 불필요한 추상화 피하기
:::
go run interfaces_example.go
# 테스트 실행go test -v
# 인터페이스 구현 확인go build # 컴파일 타임에 체크참고 자료
- Go 공식 문서 - Interfaces
- Go by Example - Interfaces
- Effective Go - Interfaces and types
- Go Blog - Laws of Reflection