# Go의 인터페이스와 다형성: 유연한 코드 작성하기
Table of Contents
목차
- 인터페이스 기본 개념
- 인터페이스 정의와 구현
- 다형성 (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
}
// 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-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