[Go] interface
Updated:
개요
인터페이스는 Go의 타입 시스템의 핵심으로, 메서드 시그니처의 집합을 정의합니다.
주요 특징:
- 메서드 집합: 타입이 구현해야 할 메서드들을 정의
- 암시적 구현: 명시적 선언 없이 자동으로 구현됨 (덕 타이핑)
- 다형성: 서로 다른 타입을 하나의 인터페이스로 처리
- 작은 인터페이스: 1-3개 메서드 권장 (단일 책임 원칙)
- 조합 가능: 인터페이스 임베딩으로 조합
- 타입 안전성: 컴파일 타임에 검증
- nil 가능: 인터페이스 변수는 nil일 수 있음
인터페이스 정의 및 구현
1. 기본 인터페이스
package main
import "fmt"
// 인터페이스 정의
type Shape interface {
Area() float64
Perimeter() float64
}
// Rectangle 구조체
type Rectangle struct {
Width float64
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)
}
// Circle 구조체
type Circle struct {
Radius float64
}
// Circle도 Shape 인터페이스를 구현
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: 10, Height: 5}
circle := Circle{Radius: 7}
printShapeInfo(rect) // Area: 50.00, Perimeter: 30.00
printShapeInfo(circle) // Area: 153.94, Perimeter: 43.98
}
2. 암시적 구현 (Duck Typing)
// 인터페이스 정의
type Writer interface {
Write([]byte) (int, error)
}
// 명시적 선언 없이 구현
type FileWriter struct {
filename string
}
func (fw FileWriter) Write(data []byte) (int, error) {
// FileWriter는 자동으로 Writer 인터페이스를 구현함
fmt.Printf("Writing %d bytes to %s\n", len(data), fw.filename)
return len(data), nil
}
func saveData(w Writer, data []byte) {
w.Write(data)
}
func main() {
fw := FileWriter{filename: "output.txt"}
saveData(fw, []byte("Hello, World!"))
}
3. 포인터 vs 값 리시버
type Counter interface {
Increment()
Value() int
}
// ❌ 값 리시버 - 원본이 수정되지 않음
type BadCounter struct {
count int
}
func (c BadCounter) Increment() {
c.count++ // 복사본만 수정됨
}
func (c BadCounter) Value() int {
return c.count
}
// ✅ 포인터 리시버 - 원본이 수정됨
type GoodCounter struct {
count int
}
func (c *GoodCounter) Increment() {
c.count++ // 원본 수정됨
}
func (c *GoodCounter) Value() int {
return c.count
}
func main() {
// BadCounter
bc := BadCounter{}
bc.Increment()
fmt.Println(bc.Value()) // 0 (변경 안됨!)
// GoodCounter
gc := &GoodCounter{}
gc.Increment()
fmt.Println(gc.Value()) // 1 (변경됨)
}
빈 인터페이스 (Empty Interface)
1. interface{} (Go 1.17 이하)
func printAnything(v interface{}) {
fmt.Println(v)
}
func main() {
printAnything(42)
printAnything("hello")
printAnything([]int{1, 2, 3})
printAnything(struct{ Name string }{"Alice"})
}
2. any (Go 1.18+)
// any는 interface{}의 별칭
func printAnything(v any) {
fmt.Println(v)
}
// 여러 타입을 담는 슬라이스
func mixedSlice() {
items := []any{
42,
"hello",
true,
3.14,
[]int{1, 2, 3},
}
for _, item := range items {
fmt.Printf("%T: %v\n", item, item)
}
}
타입 단언 (Type Assertion)
1. 기본 타입 단언
func typeAssertion() {
var i interface{} = "hello"
// 안전하지 않은 단언 (실패 시 패닉)
s := i.(string)
fmt.Println(s) // "hello"
// 안전한 단언 (Comma Ok Idiom)
s2, ok := i.(string)
if ok {
fmt.Println("String:", s2)
}
// 타입이 맞지 않으면
n, ok := i.(int)
if !ok {
fmt.Println("Not an int, got:", n) // 0
}
// ❌ 패닉 발생!
// n := i.(int) // panic: interface conversion: interface {} is string, not int
}
2. 실전 활용
func processValue(v interface{}) {
// 타입에 따라 다르게 처리
if str, ok := v.(string); ok {
fmt.Println("String length:", len(str))
return
}
if num, ok := v.(int); ok {
fmt.Println("Number doubled:", num*2)
return
}
if slice, ok := v.([]int); ok {
fmt.Println("Slice sum:", sum(slice))
return
}
fmt.Println("Unknown type")
}
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
타입 스위치 (Type Switch)
func typeSwitch(v interface{}) {
switch val := v.(type) {
case string:
fmt.Printf("String of length %d: %s\n", len(val), val)
case int:
fmt.Printf("Integer: %d\n", val)
case bool:
fmt.Printf("Boolean: %t\n", val)
case []int:
fmt.Printf("Int slice of length %d\n", len(val))
case nil:
fmt.Println("nil value")
default:
fmt.Printf("Unknown type: %T\n", val)
}
}
func main() {
typeSwitch("hello") // String of length 5: hello
typeSwitch(42) // Integer: 42
typeSwitch(true) // Boolean: true
typeSwitch([]int{1, 2}) // Int slice of length 2
typeSwitch(nil) // nil value
typeSwitch(3.14) // Unknown type: float64
}
다중 타입 처리
func handleValue(v interface{}) {
switch v.(type) {
case int, int64, int32:
fmt.Println("Some kind of int")
case string:
fmt.Println("String type")
case []byte, []rune:
fmt.Println("Byte or rune slice")
}
}
인터페이스 조합 (Interface Embedding)
// 작은 인터페이스들
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
}
func (f *File) Read(p []byte) (int, error) {
fmt.Printf("Reading from %s\n", f.name)
return len(p), nil
}
func (f *File) Write(p []byte) (int, error) {
fmt.Printf("Writing to %s\n", f.name)
return len(p), nil
}
func (f *File) Close() error {
fmt.Printf("Closing %s\n", f.name)
return nil
}
func main() {
file := &File{name: "data.txt"}
// File은 ReadWriteCloser 인터페이스를 구현
var rwc ReadWriteCloser = file
rwc.Write([]byte("hello"))
rwc.Read(make([]byte, 10))
rwc.Close()
}
표준 라이브러리 인터페이스
1. error 인터페이스
type error interface {
Error() string
}
// 커스텀 에러 타입
type ValidationError struct {
Field string
Issue string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Issue)
}
func validateAge(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Issue: "cannot be negative",
}
}
if age > 150 {
return ValidationError{
Field: "age",
Issue: "unrealistic value",
}
}
return nil
}
func main() {
if err := validateAge(-5); err != nil {
fmt.Println(err) // validation error on age: cannot be negative
// 타입 단언으로 상세 정보 접근
if ve, ok := err.(ValidationError); ok {
fmt.Printf("Field: %s, Issue: %s\n", ve.Field, ve.Issue)
}
}
}
2. fmt.Stringer 인터페이스
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // Alice (30 years old)
// Stringer가 없으면: {Alice 30}
}
3. io.Reader와 io.Writer
import (
"io"
"strings"
"os"
)
func copyData(src io.Reader, dst io.Writer) error {
_, err := io.Copy(dst, src)
return err
}
func main() {
// strings.Reader는 io.Reader 구현
reader := strings.NewReader("Hello, World!")
// os.Stdout는 io.Writer 구현
copyData(reader, os.Stdout) // Hello, World!
}
4. sort.Interface
import "sort"
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
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] }
func main() {
people := []Person{
{"Bob", 31},
{"Alice", 23},
{"Charlie", 25},
}
sort.Sort(ByAge(people))
fmt.Println(people)
// [{Alice 23} {Charlie 25} {Bob 31}]
}
인터페이스 내부 구조
1. 개념적 구조
// 인터페이스는 두 가지 구조로 구현됨
// 1. iface (메서드가 있는 인터페이스)
type iface struct {
tab *itab // 타입 정보와 메서드 테이블
data unsafe.Pointer // 실제 값의 포인터
}
// 2. eface (빈 인터페이스 interface{}/any)
type eface struct {
_type *_type // 타입 정보
data unsafe.Pointer // 실제 값의 포인터
}
2. nil 인터페이스 vs nil 값을 가진 인터페이스
func nilInterface() {
var i interface{}
fmt.Println(i == nil) // true (타입과 값 모두 nil)
var p *int
i = p
fmt.Println(i == nil) // false! (타입은 *int, 값은 nil)
// 올바른 검사
if i == nil {
fmt.Println("Interface is nil")
} else {
// 값이 nil인지 확인하려면 리플렉션 필요
if p, ok := i.(*int); ok && p == nil {
fmt.Println("Value is nil")
}
}
}
3. 인터페이스 비교
func interfaceComparison() {
var i1 interface{} = 42
var i2 interface{} = 42
fmt.Println(i1 == i2) // true
i1 = []int{1, 2, 3}
i2 = []int{1, 2, 3}
// ❌ 패닉! 슬라이스는 비교 불가능
// fmt.Println(i1 == i2) // panic: runtime error
}
실전 활용 패턴
1. 의존성 주입 (Dependency Injection)
// 인터페이스로 의존성 정의
type Database interface {
Save(key string, value interface{}) error
Load(key string) (interface{}, error)
}
// 실제 구현
type PostgresDB struct {
connString string
}
func (db *PostgresDB) Save(key string, value interface{}) error {
fmt.Printf("Saving to Postgres: %s = %v\n", key, value)
return nil
}
func (db *PostgresDB) Load(key string) (interface{}, error) {
fmt.Printf("Loading from Postgres: %s\n", key)
return "some value", nil
}
// 서비스는 인터페이스에 의존
type UserService struct {
db Database
}
func (s *UserService) SaveUser(name string) error {
return s.db.Save("user:"+name, name)
}
func main() {
db := &PostgresDB{connString: "postgres://..."}
service := &UserService{db: db}
service.SaveUser("Alice")
}
2. 모킹 (Mocking)
// 테스트용 Mock 구현
type MockDB struct {
data map[string]interface{}
}
func NewMockDB() *MockDB {
return &MockDB{
data: make(map[string]interface{}),
}
}
func (m *MockDB) Save(key string, value interface{}) error {
m.data[key] = value
return nil
}
func (m *MockDB) Load(key string) (interface{}, error) {
if val, ok := m.data[key]; ok {
return val, nil
}
return nil, fmt.Errorf("key not found")
}
func TestUserService() {
// 실제 DB 대신 Mock 사용
mockDB := NewMockDB()
service := &UserService{db: mockDB}
service.SaveUser("Bob")
if val, err := mockDB.Load("user:Bob"); err == nil {
fmt.Println("Found user:", val)
}
}
3. 전략 패턴 (Strategy Pattern)
type CompressionStrategy interface {
Compress(data []byte) []byte
}
type ZipCompression struct{}
func (z ZipCompression) Compress(data []byte) []byte {
fmt.Println("Compressing with ZIP")
return data // 실제로는 압축 로직
}
type GzipCompression struct{}
func (g GzipCompression) Compress(data []byte) []byte {
fmt.Println("Compressing with GZIP")
return data // 실제로는 압축 로직
}
type Compressor struct {
strategy CompressionStrategy
}
func (c *Compressor) SetStrategy(s CompressionStrategy) {
c.strategy = s
}
func (c *Compressor) Compress(data []byte) []byte {
return c.strategy.Compress(data)
}
func main() {
comp := &Compressor{}
data := []byte("some data")
comp.SetStrategy(ZipCompression{})
comp.Compress(data)
comp.SetStrategy(GzipCompression{})
comp.Compress(data)
}
4. 플러그인 아키텍처
type Plugin interface {
Name() string
Execute(args map[string]interface{}) error
}
type PluginRegistry struct {
plugins map[string]Plugin
}
func NewPluginRegistry() *PluginRegistry {
return &PluginRegistry{
plugins: make(map[string]Plugin),
}
}
func (r *PluginRegistry) Register(p Plugin) {
r.plugins[p.Name()] = p
}
func (r *PluginRegistry) Execute(name string, args map[string]interface{}) error {
if plugin, ok := r.plugins[name]; ok {
return plugin.Execute(args)
}
return fmt.Errorf("plugin not found: %s", name)
}
// 플러그인 구현 예제
type EmailPlugin struct{}
func (e EmailPlugin) Name() string {
return "email"
}
func (e EmailPlugin) Execute(args map[string]interface{}) error {
fmt.Printf("Sending email with args: %v\n", args)
return nil
}
type SMSPlugin struct{}
func (s SMSPlugin) Name() string {
return "sms"
}
func (s SMSPlugin) Execute(args map[string]interface{}) error {
fmt.Printf("Sending SMS with args: %v\n", args)
return nil
}
func main() {
registry := NewPluginRegistry()
registry.Register(EmailPlugin{})
registry.Register(SMSPlugin{})
registry.Execute("email", map[string]interface{}{"to": "user@example.com"})
registry.Execute("sms", map[string]interface{}{"to": "+1234567890"})
}
5. 어댑터 패턴 (Adapter Pattern)
// 레거시 타입
type LegacyPrinter struct{}
func (lp *LegacyPrinter) PrintOldWay(text string) {
fmt.Println("OLD:", text)
}
// 새로운 인터페이스
type ModernPrinter interface {
Print(text string)
}
// 어댑터
type PrinterAdapter struct {
legacy *LegacyPrinter
}
func (pa *PrinterAdapter) Print(text string) {
pa.legacy.PrintOldWay(text)
}
func usePrinter(p ModernPrinter, text string) {
p.Print(text)
}
func main() {
legacy := &LegacyPrinter{}
adapter := &PrinterAdapter{legacy: legacy}
usePrinter(adapter, "Hello, World!")
}
6. 옵저버 패턴 (Observer Pattern)
type Observer interface {
Update(message string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Attach(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(message string) {
for _, observer := range s.observers {
observer.Update(message)
}
}
type EmailObserver struct {
email string
}
func (e *EmailObserver) Update(message string) {
fmt.Printf("Email to %s: %s\n", e.email, message)
}
type SMSObserver struct {
phone string
}
func (s *SMSObserver) Update(message string) {
fmt.Printf("SMS to %s: %s\n", s.phone, message)
}
func main() {
subject := &Subject{}
subject.Attach(&EmailObserver{email: "user@example.com"})
subject.Attach(&SMSObserver{phone: "+1234567890"})
subject.Notify("System update available!")
}
제네릭과 인터페이스 (Go 1.18+)
1. 제네릭 인터페이스
type Comparable[T any] interface {
CompareTo(other T) int
}
type Number struct {
value int
}
func (n Number) CompareTo(other Number) int {
return n.value - other.value
}
func Max[T Comparable[T]](a, b T) T {
if a.CompareTo(b) > 0 {
return a
}
return b
}
2. 타입 제약 (Type Constraints)
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~string
}
func Min[T Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(10, 20)) // 10
fmt.Println(Min(3.14, 2.71)) // 2.71
fmt.Println(Min("apple", "banana")) // apple
}
3. comparable 제약
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func main() {
nums := []int{1, 2, 3, 4, 5}
fmt.Println(Contains(nums, 3)) // true
strs := []string{"a", "b", "c"}
fmt.Println(Contains(strs, "d")) // false
}
성능 고려사항
1. 인터페이스 호출 오버헤드
import "testing"
type Adder interface {
Add(a, b int) int
}
type Calculator struct{}
func (c Calculator) Add(a, b int) int {
return a + b
}
func directCall(c Calculator) int {
return c.Add(1, 2)
}
func interfaceCall(a Adder) int {
return a.Add(1, 2)
}
func BenchmarkDirectCall(b *testing.B) {
c := Calculator{}
for i := 0; i < b.N; i++ {
directCall(c)
}
}
func BenchmarkInterfaceCall(b *testing.B) {
var a Adder = Calculator{}
for i := 0; i < b.N; i++ {
interfaceCall(a)
}
}
// 인터페이스 호출은 직접 호출보다 약간 느림 (나노초 단위)
// 대부분의 경우 무시할 수 있는 수준
2. 빈 인터페이스 vs 제네릭
// ❌ 빈 인터페이스 - 런타임 타입 단언 필요
func SumAny(items []interface{}) int {
sum := 0
for _, item := range items {
if num, ok := item.(int); ok {
sum += num
}
}
return sum
}
// ✅ 제네릭 - 컴파일 타임에 타입 안전
func Sum[T ~int | ~int64](items []T) T {
var sum T
for _, item := range items {
sum += item
}
return sum
}
// 제네릭이 더 안전하고 빠름
3. 인터페이스 값 vs 포인터
type Data struct {
value [1024]byte // 큰 구조체
}
type Processor interface {
Process()
}
// ❌ 값 리시버 - 구조체 복사 발생
func (d Data) Process() {
// 1024 바이트 복사
}
// ✅ 포인터 리시버 - 포인터만 복사
func (d *Data) Process() {
// 8 바이트 포인터만 복사
}
// 큰 구조체는 포인터 리시버 사용 권장
일반적인 실수
1. nil 인터페이스 혼동
func mistake1() {
var p *int = nil
var i interface{} = p
// ❌ false! (타입 정보가 있음)
fmt.Println(i == nil) // false
// ✅ 올바른 검사
if i == nil {
fmt.Println("Interface is nil")
} else {
fmt.Println("Interface is not nil, but value might be")
}
}
func returnNilInterface() interface{} {
var p *int = nil
return p // ❌ nil이 아닌 인터페이스 반환!
}
func main() {
result := returnNilInterface()
if result == nil {
fmt.Println("nil") // 출력되지 않음!
} else {
fmt.Println("not nil") // 출력됨
}
}
2. 타입 단언 없이 panic
func mistake2() {
var i interface{} = "hello"
// ❌ 패닉 발생!
// num := i.(int) // panic
// ✅ 안전한 방법
num, ok := i.(int)
if !ok {
fmt.Println("Not an int")
} else {
fmt.Println(num)
}
}
3. 빈 인터페이스 남용
// ❌ 타입 안전성 상실
func badFunction(data interface{}) {
// 런타임에 타입 검사 필요
if str, ok := data.(string); ok {
// ...
}
}
// ✅ 구체적인 타입 또는 제네릭 사용
func goodFunction(data string) {
// 컴파일 타임에 타입 안전
}
func genericFunction[T any](data T) {
// 제네릭으로 타입 안전성 유지
}
4. 인터페이스를 반환할 때 nil 처리
type MyError struct {
msg string
}
func (e *MyError) Error() string {
return e.msg
}
// ❌ 잘못된 패턴
func badErrorHandling() error {
var err *MyError = nil
if someCondition := false; someCondition {
err = &MyError{msg: "error"}
}
return err // nil *MyError를 반환하지만 error 인터페이스는 nil이 아님!
}
// ✅ 올바른 패턴
func goodErrorHandling() error {
if someCondition := false; someCondition {
return &MyError{msg: "error"}
}
return nil // nil 인터페이스 반환
}
func main() {
if err := badErrorHandling(); err != nil {
fmt.Println("Error occurred") // 항상 실행됨!
}
if err := goodErrorHandling(); err != nil {
fmt.Println("Error occurred") // 조건에 따라 실행
}
}
5. 큰 인터페이스 정의
// ❌ 너무 큰 인터페이스
type BadRepository interface {
Create(entity interface{}) error
Read(id int) (interface{}, error)
Update(entity interface{}) error
Delete(id int) error
List() ([]interface{}, error)
Search(query string) ([]interface{}, error)
Count() int
// ... 더 많은 메서드
}
// ✅ 작은 인터페이스로 분리
type Creator interface {
Create(entity interface{}) error
}
type Reader interface {
Read(id int) (interface{}, error)
}
type Updater interface {
Update(entity interface{}) error
}
type Deleter interface {
Delete(id int) error
}
// 필요한 것만 조합
type Repository interface {
Creator
Reader
Updater
Deleter
}
6. 포인터 리시버와 값 리시버 혼용
type Counter struct {
count int
}
// 값 리시버
func (c Counter) Get() int {
return c.count
}
// 포인터 리시버
func (c *Counter) Increment() {
c.count++
}
type Getter interface {
Get() int
}
type Incrementer interface {
Increment()
}
func mistake6() {
c := Counter{}
// ✅ OK
var g Getter = c
_ = g
// ❌ 컴파일 에러: Counter는 Incrementer 구현 안 함
// var inc Incrementer = c
// ✅ OK: 포인터는 가능
var inc Incrementer = &c
_ = inc
}
인터페이스 설계 원칙
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 ReadWriteCloser interface {
Reader
Writer
Closer
}
2. Accept Interfaces, Return Structs
// ✅ 함수는 인터페이스를 받음
func ProcessData(r io.Reader) error {
// ...
return nil
}
// ✅ 함수는 구체 타입을 반환
func NewReader(filename string) (*FileReader, error) {
// ...
return &FileReader{}, nil
}
// ❌ 인터페이스 반환은 피하기 (필요한 경우 제외)
func NewReaderBad(filename string) (io.Reader, error) {
// 테스트 어려움, 타입 단언 필요
return &FileReader{}, nil
}
3. 클라이언트가 인터페이스를 정의
// 라이브러리 코드 (구체 타입 제공)
type HTTPClient struct {
// ...
}
func (c *HTTPClient) Get(url string) (*Response, error) {
// ...
return nil, nil
}
func (c *HTTPClient) Post(url string, body []byte) (*Response, error) {
// ...
return nil, nil
}
// 클라이언트 코드 (필요한 인터페이스만 정의)
type MyHTTPClient interface {
Get(url string) (*Response, error)
// Post는 사용하지 않으므로 포함 안 함
}
func fetchData(client MyHTTPClient) {
client.Get("https://example.com")
}
정리
- 인터페이스는 메서드 시그니처의 집합
- 암시적 구현:
implements키워드 없음 - 작은 인터페이스 권장 (1-3개 메서드)
- 타입 단언:
value, ok := i.(Type)패턴 사용 - 타입 스위치:
switch v.(type)으로 타입별 처리 - 인터페이스 조합: 임베딩으로 작은 인터페이스 조합
- 표준 인터페이스: error, Stringer, Reader, Writer 등
- nil 인터페이스: 타입과 값이 모두 nil이어야
== nil - 의존성 주입: 인터페이스로 결합도 낮춤
- Accept interfaces, return structs 원칙
- 큰 구조체는 포인터 리시버 사용
- 빈 인터페이스 남용 피하기 (제네릭 고려)
- 클라이언트가 인터페이스를 정의하는 것이 유연함