# Go 디자인 패턴: Go 개발자를 위한 필수 패턴과 면접 대비 가이드 > Go의 설계 철학에 기반한 디자인 패턴을 체계적으로 해설합니다. Functional Options, Strategy, Observer, Middleware 등 면접에서 자주 출제되는 실전 패턴과 답변 전략을 다룹니다. - Published: 2026-05-28 - Updated: 2026-05-28 - Author: SharpSkill - Tags: go, design-patterns, interview, best-practices - Reading time: 12 min --- Go의 디자인 패턴은 객체지향 언어의 패턴과 근본적으로 다릅니다. 클래스나 상속 없이 Go는 컴포지션, 인터페이스, 일급 함수를 활용하여 동일한 구조적 유연성을 달성합니다. 흔히 적은 코드와 더 높은 명확성으로 이를 구현합니다. > **Go 설계 원칙** > > Go는 상속보다 컴포지션을 우선합니다. 모든 전통적인 디자인 패턴은 클래스 계층 구조가 아닌 인터페이스, 구조체 임베딩, 일급 함수를 활용하여 적용해야 합니다. ## Functional Options 패턴: 유연한 설정 구현 Functional Options 패턴은 Go에서 자주 발생하는 문제를 해결합니다. 많은 선택적 매개변수를 가진 객체를 생성할 때, 메서드 오버로딩이나 기본 인수가 없는 Go에서는 이를 처리하기가 까다롭습니다. Builder 패턴도 가능하지만 장황해지기 쉽습니다. Functional Options는 요구사항이 증가해도 하위 호환성을 유지하는 깔끔하고 확장 가능한 API를 제공합니다. 이 패턴은 [Dave Cheney가 대중화](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)했으며, 현재 Go 생태계 전반에서 표준으로 사용됩니다. 가변 인수 함수를 사용하여 구조체를 설정하는 방식입니다. ```go // server.go package server import ( "time" "log/slog" ) // Server holds the HTTP server configuration. type Server struct { host string port int timeout time.Duration maxConns int logger *slog.Logger } // Option defines a functional option for Server. type Option func(*Server) // WithPort sets the server port. func WithPort(port int) Option { return func(s *Server) { s.port = port } } // WithTimeout sets the request timeout. func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } // WithMaxConns sets the maximum concurrent connections. func WithMaxConns(n int) Option { return func(s *Server) { s.maxConns = n } } // New creates a Server with sensible defaults and applies options. func New(host string, opts ...Option) *Server { srv := &Server{ host: host, port: 8080, // default port timeout: 30 * time.Second, // default timeout maxConns: 100, // default max connections logger: slog.Default(), } for _, opt := range opts { opt(srv) } return srv } ``` 호출하는 쪽에서는 기본값과 다른 부분만 지정하면 됩니다. 새로운 옵션을 추가해도 기존 호출 코드에 영향을 주지 않습니다. 이 패턴은 `google.golang.org/grpc`와 `go.uber.org/zap` 같은 프로덕션 라이브러리에서도 사용됩니다. ## Strategy 패턴: 인터페이스 기반 다형성 Strategy 패턴은 교체 가능한 알고리즘을 공통 인터페이스 뒤에 캡슐화합니다. Go에서는 추상 클래스나 상속 체인 없이 인터페이스 기반 다형성으로 직접 구현할 수 있습니다. 결제 처리 시스템이 이 패턴의 좋은 예시입니다. 각 결제 방식이 동일한 인터페이스를 구현하고, 체크아웃 서비스가 런타임에 전략을 선택합니다. ```go // payment.go package payment import "fmt" // Processor defines the strategy interface. type Processor interface { Pay(amount float64) (string, error) } // CreditCard implements Processor for card payments. type CreditCard struct { CardNumber string Expiry string } func (c *CreditCard) Pay(amount float64) (string, error) { // Charge the card via payment gateway return fmt.Sprintf("charged %.2f to card ending %s", amount, c.CardNumber[len(c.CardNumber)-4:]), nil } // BankTransfer implements Processor for wire transfers. type BankTransfer struct { IBAN string } func (b *BankTransfer) Pay(amount float64) (string, error) { return fmt.Sprintf("initiated transfer of %.2f to %s", amount, b.IBAN), nil } // Checkout processes a payment using the given strategy. func Checkout(p Processor, amount float64) error { receipt, err := p.Pay(amount) if err != nil { return fmt.Errorf("payment failed: %w", err) } fmt.Println(receipt) return nil } ``` Go 인터페이스는 암묵적으로 충족됩니다. `implements` 키워드가 필요하지 않습니다. `Pay(float64) (string, error)` 메서드를 가진 모든 타입이 `Processor`로 사용될 수 있습니다. 이로 인해 결합도가 낮아지고 테스트가 간편해집니다. 예측 가능한 결과를 반환하는 mock `Processor`를 전달하면 됩니다. ## 팩토리 함수와 생성자 패턴 Go에는 생성자가 없습니다. 관용적인 대안은 `New` 또는 `NewXxx`로 명명된 팩토리 함수로, 초기화된 구조체를 반환합니다. 팩토리 함수는 제로 값 초기화로는 보장할 수 없는 불변 조건을 강제합니다. 면접 준비에서 이 구분은 중요합니다. 팩토리 함수가 필요한 경우와 [제로 값이 기본으로 유용한 경우](https://go.dev/blog/organizing-go-code)를 올바르게 판단하는 능력이 요구됩니다. ```go // connpool.go package pool import ( "errors" "sync" ) // ConnPool manages a pool of reusable connections. type ConnPool struct { mu sync.Mutex conns []Conn maxSize int } // Conn represents a database connection. type Conn struct { ID int Active bool } // NewConnPool validates parameters and returns an initialized pool. func NewConnPool(maxSize int) (*ConnPool, error) { if maxSize <= 0 { return nil, errors.New("pool: maxSize must be positive") } return &ConnPool{ conns: make([]Conn, 0, maxSize), maxSize: maxSize, }, nil } // Acquire returns a connection from the pool. func (p *ConnPool) Acquire() (*Conn, error) { p.mu.Lock() defer p.mu.Unlock() for i := range p.conns { if !p.conns[i].Active { p.conns[i].Active = true return &p.conns[i], nil } } if len(p.conns) >= p.maxSize { return nil, errors.New("pool: no available connections") } c := Conn{ID: len(p.conns) + 1, Active: true} p.conns = append(p.conns, c) return &p.conns[len(p.conns)-1], nil } ``` 팩토리 함수 `NewConnPool`은 `maxSize` 매개변수를 검증하고 슬라이스를 사전 할당합니다. 구조체를 직접 초기화(`&ConnPool{}`)하면 검증이 생략되어 런타임 패닉이 발생할 수 있습니다. 이 패턴은 Functional Options와 결합하여 더 복잡한 설정 시나리오에도 대응할 수 있습니다. ## Observer 패턴: 채널과 고루틴을 활용한 구현 Observer 패턴은 상태가 변경될 때 여러 구독자에게 알림을 보냅니다. Java나 C#에서는 이벤트 리스너와 콜백 등록이 필요하지만, Go는 더 관용적인 방법을 제공합니다. 바로 채널입니다. 각 구독자는 자신만의 채널을 통해 이벤트를 수신하고, 고루틴이 동시에 이벤트를 전달합니다. 이 접근 방식은 Go의 [동시성 프리미티브](/blog/go/go-concurrency-goroutines-channels)를 직접 활용하여 콜백 스파게티를 방지합니다. ```go // eventbus.go package events import "sync" // Event carries a topic and payload. type Event struct { Topic string Payload any } // Bus manages subscriptions and event dispatch. type Bus struct { mu sync.RWMutex subscribers map[string][]chan Event } // NewBus creates an event bus. func NewBus() *Bus { return &Bus{ subscribers: make(map[string][]chan Event), } } // Subscribe returns a channel that receives events for a topic. func (b *Bus) Subscribe(topic string) <-chan Event { ch := make(chan Event, 16) // buffered to avoid blocking publisher b.mu.Lock() b.subscribers[topic] = append(b.subscribers[topic], ch) b.mu.Unlock() return ch } // Publish sends an event to all subscribers of the topic. func (b *Bus) Publish(topic string, payload any) { b.mu.RLock() defer b.mu.RUnlock() for _, ch := range b.subscribers[topic] { select { case ch <- Event{Topic: topic, Payload: payload}: default: // subscriber too slow, drop event } } } ``` `select`문에 `default` 케이스를 사용하여 느린 구독자가 전체 버스를 차단하는 것을 방지합니다. 프로덕션 시스템에서는 context 기반 취소 메커니즘과 `Unsubscribe` 메서드를 추가하여 구현을 완성해야 합니다. ## Middleware 패턴: HTTP 요청 파이프라인 미들웨어 체인은 Go HTTP 서버의 핵심입니다. 이 패턴은 `http.Handler`를 래핑하여 로깅, 인증, 속도 제한 등의 추가 동작을 핸들러 자체를 수정하지 않고 부여합니다. 표준 라이브러리의 `http.Handler` 인터페이스 덕분에 이러한 구성이 매우 간편합니다. ```go // middleware.go package middleware import ( "log/slog" "net/http" "time" ) // Logging records request duration and status. func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrapped := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(wrapped, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", wrapped.status, "duration", time.Since(start), ) }) } // statusWriter captures the HTTP status code. type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } // Chain applies middleware in order: first listed = outermost. func Chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler { for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) } return h } ``` `Chain` 함수는 미들웨어를 선언 순서대로 적용합니다. 각 미들웨어가 다음 핸들러를 래핑하여 파이프라인을 형성합니다. 이 패턴은 [chi 같은 인기 라우터](https://github.com/go-chi/chi)에서도 동일하며, `func(http.Handler) http.Handler` 시그니처의 미들웨어를 수용합니다. ## 구조체 임베딩을 통한 컴포지션 Go는 상속을 지원하지 않습니다. 대신 구조체 임베딩(struct embedding)을 사용하여 내부 타입의 메서드를 외부 타입으로 승격시키고, 강한 결합 없이 코드를 재사용합니다. 이것은 상속이 아닙니다. 임베딩된 타입은 임베딩하는 타입에 대해 아무것도 알지 못하며, 가상 디스패치도 발생하지 않습니다. 이 구분을 이해하는 것은 [Go 면접](/technologies/go/interview-questions/concurrency-patterns)에서 매우 중요합니다. 임베딩과 상속을 혼동하는 후보자는 Go의 타입 시스템에 대한 이해가 피상적이라는 평가를 받게 됩니다. ```go // models.go package models import "time" // Timestamps provides common audit fields. type Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } // User embeds Timestamps to gain CreatedAt/UpdatedAt. type User struct { Timestamps ID int Email string } // Order also embeds Timestamps. type Order struct { Timestamps ID int UserID int Total float64 } ``` `User`와 `Order` 모두 `CreatedAt`과 `UpdatedAt` 필드에 직접 접근할 수 있습니다. `Timestamps`에 정의된 메서드도 마찬가지로 승격됩니다. 핵심 규칙은 임베딩이 위임을 제공하는 것이지 대체가 아니라는 점입니다. `User`는 `Timestamps`가 아니라 `Timestamps`를 가지고 있는 것입니다. ## Go 디자인 패턴 면접 질문 Go 면접에서 디자인 패턴 질문은 후보자가 클래식 패턴을 Go의 타입 시스템에 적용할 수 있는지 확인합니다. 다음은 기술 스크리닝과 현장 면접에서 자주 등장하는 질문들입니다. **Q: 팩토리 함수가 패닉 대신 에러를 반환해야 하는 경우는 언제인가?** 팩토리 함수는 검증이 런타임 입력(사용자 제공 설정, 환경 변수, 외부 데이터)에 의존하는 경우 에러를 반환해야 합니다. 패닉은 프로그래머 오류, 즉 API 계약에서 금지하는 nil 포인터를 전달하는 것과 같이 버그를 나타내는 상황에만 사용됩니다. 표준 라이브러리도 이 관례를 따릅니다. `os.Open`은 에러를 반환하고, `regexp.MustCompile`은 컴파일 타임 상수 패턴을 기대하기 때문에 패닉합니다. **Q: Functional Options 패턴은 API 발전을 어떻게 개선하는가?** 새로운 `WithXxx` 함수를 추가하는 것은 하위 호환성을 깨뜨리지 않는 변경입니다. 기존 호출 코드는 수정 없이 그대로 동작합니다. 이는 설정 구조체(필수 필드 추가 시 모든 호출 지점이 깨짐)나 위치 매개변수(인수 순서 변경이 버그를 유발)와 대조됩니다. **Q: Go 인터페이스가 Java나 C# 인터페이스와 다른 점은 무엇인가?** Go 인터페이스는 암묵적으로 충족됩니다. 타입이 필요한 메서드를 가지기만 하면 인터페이스를 구현한 것이며, `implements` 선언이 필요하지 않습니다. 이를 통해 "인터페이스를 받고, 구조체를 반환한다"는 원칙이 가능합니다. 즉, 구현 측이 아닌 호출 측에서 좁은 인터페이스를 정의합니다. 그 결과 [낮은 결합도와 높은 테스트 용이성](/technologies/go/interview-questions/testing)이 달성됩니다. > **면접 패턴** > > 면접관은 후보자에게 구체적인 의존성을 인터페이스로 리팩토링하도록 자주 요청합니다. 테스트 포인트는 필요한 최소한의 메서드 세트를 식별하고, 구현 측이 아닌 소비자 측에서 인터페이스를 추출할 수 있는지 여부입니다. **Q: 채널 기반 Observer와 콜백 기반 Observer의 차이점은 무엇인가?** 채널은 발행자와 구독자를 시간과 공간 모두에서 분리합니다. 발행자는 구독자 함수에 대한 참조를 보유하지 않고 채널에 전송만 합니다. 구독자는 버퍼링된 채널을 사용하여 자신의 속도로 이벤트를 처리할 수 있습니다. `select`문을 통해 타임아웃 처리, `context`를 통한 취소, 여러 이벤트 소스의 멀티플렉싱이 가능합니다. 이러한 기능은 콜백으로는 쉽게 얻을 수 없습니다. > **주의사항** > > 구독자 채널을 닫는 것을 잊으면 고루틴 누수가 발생합니다. 모든 `Subscribe`에는 채널을 닫고 버스에서 제거하는 대응하는 `Unsubscribe`가 있어야 합니다. ## 결론 - Functional Options 패턴은 Builder와 설정 구조체를 기존 호출 코드를 깨뜨리지 않는 깔끔하고 확장 가능한 API로 대체합니다 - Go의 Strategy 패턴은 인터페이스 기반 다형성에 대응합니다. 인터페이스 정의는 구현 측이 아닌 소비자 측에서 수행합니다 - 팩토리 함수는 제로 값 초기화로는 보장할 수 없는 불변 조건을 강제합니다. 런타임 입력에는 에러를 반환하고, 패닉은 프로그래머 버그에만 사용합니다 - 채널 기반 Observer는 고루틴과 select를 활용하여 콜백 체인 없이 동시적이고 느슨하게 결합된 이벤트 전달을 구현합니다 - HTTP 미들웨어 체인은 `func(http.Handler) http.Handler` 시그니처를 통해 구성되며, 표준 라이브러리와 서드파티 라우터에서 동일합니다 - 구조체 임베딩은 위임을 제공하는 것이지 상속이 아닙니다. 이 구분을 이해하는 것이 Go를 깊이 이해하는 개발자와 Java 패턴을 그대로 옮기는 개발자를 구분합니다 - 면접에서의 성공은 GoF 정의의 암기가 아니라 패턴을 Go의 타입 시스템에 관용적으로 적용하는 능력에 달려 있습니다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/go/go-design-patterns-essential-patterns-interview