본문으로 건너뛰기

실전 고수 팁 — 인터페이스와 제네릭

인터페이스 설계 원칙 — 작게 만들어라

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. 인터페이스는 작게— 1~3개 메서드가 이상적, 단일 메서드 인터페이스가 가장 강력
  2. 인터페이스는 사용하는 쪽에서 정의— 구현 패키지가 아닌 호출 패키지에
  3. nil 인터페이스 함정— 구체 타입 nil을 인터페이스로 반환하면 nil이 아님
  4. 제네릭은 데이터 구조/알고리즘— 동작 추상화는 인터페이스, 타입 범용화는 제네릭
  5. ~T 제약— 기반 타입이 같은 사용자 정의 타입까지 포함
  6. 타입 추론 활용— 대부분의 경우 타입 파라미터를 명시하지 않아도 됨
  7. 제네릭 남용 주의any로 충분하면 제네릭 불필요, 인터페이스가 더 명확하면 인터페이스