실전 고수 팁 — 인터페이스와 제네릭
인터페이스 설계 원칙 — 작게 만들어라
package main
import (
"fmt"
"io"
"strings"
)
// 나쁜 예: 지나치게 큰 인터페이스
type BadRepository interface {
FindByID(id int) (any, error)
FindAll() ([]any, error)
Create(entity any) error
Update(entity any) error
Delete(id int) error
Count() (int, error)
Search(query string) ([]any, error)
// 사용자가 이 모든 메서드를 구현해야 함 → 테스트하기 어려움
}
// 좋은 예: 역할별로 작게 분리
type Finder interface {
FindByID(id int) (any, error)
}
type Creator interface {
Create(entity any) error
}
type Updater interface {
Update(entity any) error
}
type Deleter interface {
Delete(id int) error
}
// 필요에 따라 조합
type ReadRepository interface {
Finder
FindAll() ([]any, error)
}
type WriteRepository interface {
Creator
Updater
Deleter
}
// 인터페이스는 사용하는 쪽(consumer)에서 정의하는 것이 Go 스타일
// 구현체 패키지가 아닌 호출 패키지에 인터페이스 정의
// 실전: 테스트하기 쉬운 의존성 주입
type EmailSender interface {
Send(to, subject, body string) error
}
type UserService struct {
emailer EmailSender
}
func NewUserService(emailer EmailSender) *UserService {
return &UserService{emailer: emailer}
}
func (s *UserService) Register(email string) error {
// 비즈니스 로직...
return s.emailer.Send(email, "환영합니다", "가입을 축영합니다!")
}
// 테스트용 Mock
type MockEmailSender struct {
Sent []string
}
func (m *MockEmailSender) Send(to, subject, body string) error {
m.Sent = append(m.Sent, to)
fmt.Printf("[MOCK] 이메일 전송 → %s\n", to)
return nil
}
// 실제 구현
type SMTPSender struct {
host string
}
func (s *SMTPSender) Send(to, subject, body string) error {
// 실제로는 SMTP 전송
fmt.Printf("[SMTP] %s → %s: %s\n", s.host, to, subject)
return nil
}
func main() {
// 테스트 환경
mock := &MockEmailSender{}
svc := NewUserService(mock)
svc.Register("user@example.com")
fmt.Println("전송된 이메일:", mock.Sent)
// 프로덕션 환경
smtp := &SMTPSender{host: "mail.example.com"}
prodSvc := NewUserService(smtp)
prodSvc.Register("user2@example.com")
// io.Writer도 인터페이스 — 어디든 쓸 수 있음
writers := []io.Writer{
&strings.Builder{},
// os.Stdout, bufio.NewWriter(...), etc.
}
_ = writers
}
인터페이스 nil 함정 — 완전한 해설
package main
import "fmt"
type MyError struct {
msg string
}
func (e *MyError) Error() string { return e.msg }
// 흔한 실수: nil 포인터를 인터페이스로 반환
func badFunc(fail bool) error {
var err *MyError // nil *MyError
if fail {
err = &MyError{"실패했습니다"}
}
return err // (*MyError)(nil) → error 인터페이스는 nil이 아님!
}
// 올바른 방법: error를 직접 nil로 반환
func goodFunc(fail bool) error {
if fail {
return &MyError{"실패했습니다"}
}
return nil // error(nil) — 진짜 nil 인터페이스
}
func main() {
// 함정: badFunc(false)는 nil처럼 보이지만 nil이 아님
err1 := badFunc(false)
fmt.Println("badFunc 결과:", err1) // <nil>
fmt.Println("badFunc nil 체크:", err1 == nil) // false! (버그)
err2 := goodFunc(false)
fmt.Println("goodFunc 결과:", err2) // <nil>
fmt.Println("goodFunc nil 체크:", err2 == nil) // true
// 인터페이스 내부 구조 이해
// 인터페이스 = (타입 포인터, 값 포인터)
// badFunc: (타입=*MyError, 값=nil) → nil이 아님
// goodFunc: (타입=nil, 값=nil) → nil
// 실무에서 자주 나타나는 또 다른 함정
var slice []int = nil
var m map[string]int = nil
// 이것들은 안전 — 슬라이스/맵은 인터페이스가 아님
fmt.Println(slice == nil) // true
fmt.Println(m == nil) // true
// 하지만 any에 담으면
var iSlice any = slice
fmt.Println(iSlice == nil) // false! — (타입=[]int, 값=nil)
}
제네릭 남용 vs 올바른 사용
package main
import (
"fmt"
"sort"
)
// 상황 1: 인터페이스가 더 적합
// 나쁜 예
func GenericProcess[T any](v T) {
fmt.Println(v) // 제네릭 의미 없음 — any로 충분
}
// 좋은 예
type Processor interface {
Process() string
}
func InterfaceProcess(p Processor) {
fmt.Println(p.Process())
}
// 상황 2: 제네릭이 확실히 유리한 경우
// 타입 안전한 컬렉션 유틸리티
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func Unique[T comparable](slice []T) []T {
seen := make(map[T]struct{})
result := make([]T, 0, len(slice))
for _, v := range slice {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
result = append(result, v)
}
}
return result
}
func Chunk[T any](slice []T, size int) [][]T {
if size <= 0 {
return nil
}
var chunks [][]T
for size < len(slice) {
slice, chunks = slice[size:], append(chunks, slice[:size])
}
return append(chunks, slice)
}
// 상황 3: 타입 파라미터 추론 활용
func Zip[A, B any](as []A, bs []B) []struct{ A A; B B } {
n := len(as)
if len(bs) < n {
n = len(bs)
}
result := make([]struct{ A A; B B }, n)
for i := 0; i < n; i++ {
result[i] = struct{ A A; B B }{as[i], bs[i]}
}
return result
}
func main() {
nums := []int{1, 2, 3, 2, 4, 1, 5, 3}
fmt.Println("Contains 3:", Contains(nums, 3)) // true
fmt.Println("Contains 9:", Contains(nums, 9)) // false
fmt.Println("Unique:", Unique(nums)) // [1 2 3 4 5]
fmt.Println("Chunk(3):", Chunk(nums, 3)) // [[1 2 3] [2 4 1] [5 3]]
strs := []string{"a", "b", "c", "b", "a"}
fmt.Println("Unique strs:", Unique(strs)) // [a b c]
keys := []string{"name", "age", "city"}
vals := []int{1, 30, 2}
zipped := Zip(keys, vals)
for _, z := range zipped {
fmt.Printf("%s: %d\n", z.A, z.B)
}
// sort.Slice는 제네릭 없이도 유연
words := []string{"banana", "apple", "cherry", "date"}
sort.Slice(words, func(i, j int) bool {
return words[i] < words[j]
})
fmt.Println(words)
}
인터페이스 컴포지션으로 미들웨어 구축
package main
import (
"fmt"
"time"
)
// 핵심 인터페이스
type Handler interface {
Handle(req string) string
}
// 함수 타입으로 인터페이스 구현
type HandlerFunc func(string) string
func (f HandlerFunc) Handle(req string) string {
return f(req)
}
// 미들웨어 체인 빌더
type MiddlewareChain struct {
handler Handler
middlewares []func(Handler) Handler
}
func NewChain(h Handler) *MiddlewareChain {
return &MiddlewareChain{handler: h}
}
func (c *MiddlewareChain) Use(mw func(Handler) Handler) *MiddlewareChain {
c.middlewares = append(c.middlewares, mw)
return c
}
func (c *MiddlewareChain) Build() Handler {
h := c.handler
// 역순으로 적용 (LIFO)
for i := len(c.middlewares) - 1; i >= 0; i-- {
h = c.middlewares[i](h)
}
return h
}
// 미들웨어들
func LoggingMiddleware(next Handler) Handler {
return HandlerFunc(func(req string) string {
start := time.Now()
result := next.Handle(req)
fmt.Printf("[LOG] req=%q result=%q elapsed=%v\n",
req, result, time.Since(start))
return result
})
}
func CachingMiddleware(next Handler) Handler {
cache := make(map[string]string)
return HandlerFunc(func(req string) string {
if cached, ok := cache[req]; ok {
fmt.Printf("[CACHE HIT] %q\n", req)
return cached
}
result := next.Handle(req)
cache[req] = result
return result
})
}
func RetryMiddleware(maxRetries int) func(Handler) Handler {
return func(next Handler) Handler {
return HandlerFunc(func(req string) string {
for i := 0; i < maxRetries; i++ {
result := next.Handle(req)
if result != "ERROR" {
return result
}
fmt.Printf("[RETRY] attempt %d/%d\n", i+1, maxRetries)
}
return "FAILED"
})
}
}
func main() {
// 기본 핸들러
base := HandlerFunc(func(req string) string {
return "처리됨: " + req
})
// 미들웨어 체인 구성
handler := NewChain(base).
Use(LoggingMiddleware).
Use(CachingMiddleware).
Build()
// 요청 처리
fmt.Println(handler.Handle("hello"))
fmt.Println(handler.Handle("world"))
fmt.Println(handler.Handle("hello")) // 캐시 히트
}
제네릭과 인터페이스 선택 기준 요약표
package main
import "fmt"
// 1. 런타임 다형성이 필요한가? → 인터페이스
type Logger interface {
Log(msg string)
}
// 2. 컴파일 타임 타입 안전성이 필요한가? → 제네릭
type TypedSlice[T any] struct {
items []T
}
// 3. 행동(동작)을 추상화? → 인터페이스
type Sorter interface {
Sort()
}
// 4. 데이터 구조를 범용화? → 제네릭
type Queue[T any] struct {
items []T
}
func (q *Queue[T]) Enqueue(item T) { q.items = append(q.items, item) }
func (q *Queue[T]) Dequeue() (T, bool) {
if len(q.items) == 0 {
var zero T
return zero, false
}
item := q.items[0]
q.items = q.items[1:]
return item, true
}
// 5. 외부 패키지 타입도 지원? → 인터페이스 (구현만 하면 됨)
// 6. 성능이 중요? → 제네릭 (인터페이스의 동적 디스패치 오버헤드 없음)
func main() {
q := &Queue[string]{}
q.Enqueue("첫 번째")
q.Enqueue("두 번째")
q.Enqueue("세 번째")
for {
item, ok := q.Dequeue()
if !ok {
break
}
fmt.Println(item)
}
}
핵심 규칙
- 인터페이스는 작게— 1~3개 메서드가 이상적, 단일 메서드 인터페이스가 가장 강력
- 인터페이스는 사용하는 쪽에서 정의— 구현 패키지가 아닌 호출 패키지에
- nil 인터페이스 함정— 구체 타입 nil을 인터페이스로 반환하면 nil이 아님
- 제네릭은 데이터 구조/알고리즘— 동작 추상화는 인터페이스, 타입 범용화는 제네릭
~T제약— 기반 타입이 같은 사용자 정의 타입까지 포함- 타입 추론 활용— 대부분의 경우 타입 파라미터를 명시하지 않아도 됨
- 제네릭 남용 주의—
any로 충분하면 제네릭 불필요, 인터페이스가 더 명확하면 인터페이스