Go 디자인 패턴: Go 개발자를 위한 필수 패턴과 면접 대비 가이드
Go의 설계 철학에 기반한 디자인 패턴을 체계적으로 해설합니다. Functional Options, Strategy, Observer, Middleware 등 면접에서 자주 출제되는 실전 패턴과 답변 전략을 다룹니다.

Go의 디자인 패턴은 객체지향 언어의 패턴과 근본적으로 다릅니다. 클래스나 상속 없이 Go는 컴포지션, 인터페이스, 일급 함수를 활용하여 동일한 구조적 유연성을 달성합니다. 흔히 적은 코드와 더 높은 명확성으로 이를 구현합니다.
Go는 상속보다 컴포지션을 우선합니다. 모든 전통적인 디자인 패턴은 클래스 계층 구조가 아닌 인터페이스, 구조체 임베딩, 일급 함수를 활용하여 적용해야 합니다.
Functional Options 패턴: 유연한 설정 구현
Functional Options 패턴은 Go에서 자주 발생하는 문제를 해결합니다. 많은 선택적 매개변수를 가진 객체를 생성할 때, 메서드 오버로딩이나 기본 인수가 없는 Go에서는 이를 처리하기가 까다롭습니다. Builder 패턴도 가능하지만 장황해지기 쉽습니다. Functional Options는 요구사항이 증가해도 하위 호환성을 유지하는 깔끔하고 확장 가능한 API를 제공합니다.
이 패턴은 Dave Cheney가 대중화했으며, 현재 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에서는 추상 클래스나 상속 체인 없이 인터페이스 기반 다형성으로 직접 구현할 수 있습니다.
결제 처리 시스템이 이 패턴의 좋은 예시입니다. 각 결제 방식이 동일한 인터페이스를 구현하고, 체크아웃 서비스가 런타임에 전략을 선택합니다.
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로 명명된 팩토리 함수로, 초기화된 구조체를 반환합니다. 팩토리 함수는 제로 값 초기화로는 보장할 수 없는 불변 조건을 강제합니다.
면접 준비에서 이 구분은 중요합니다. 팩토리 함수가 필요한 경우와 제로 값이 기본으로 유용한 경우를 올바르게 판단하는 능력이 요구됩니다.
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와 결합하여 더 복잡한 설정 시나리오에도 대응할 수 있습니다.
Go 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
Observer 패턴: 채널과 고루틴을 활용한 구현
Observer 패턴은 상태가 변경될 때 여러 구독자에게 알림을 보냅니다. Java나 C#에서는 이벤트 리스너와 콜백 등록이 필요하지만, Go는 더 관용적인 방법을 제공합니다. 바로 채널입니다. 각 구독자는 자신만의 채널을 통해 이벤트를 수신하고, 고루틴이 동시에 이벤트를 전달합니다.
이 접근 방식은 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 인터페이스 덕분에 이러한 구성이 매우 간편합니다.
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 같은 인기 라우터에서도 동일하며, func(http.Handler) http.Handler 시그니처의 미들웨어를 수용합니다.
구조체 임베딩을 통한 컴포지션
Go는 상속을 지원하지 않습니다. 대신 구조체 임베딩(struct embedding)을 사용하여 내부 타입의 메서드를 외부 타입으로 승격시키고, 강한 결합 없이 코드를 재사용합니다. 이것은 상속이 아닙니다. 임베딩된 타입은 임베딩하는 타입에 대해 아무것도 알지 못하며, 가상 디스패치도 발생하지 않습니다.
이 구분을 이해하는 것은 Go 면접에서 매우 중요합니다. 임베딩과 상속을 혼동하는 후보자는 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 선언이 필요하지 않습니다. 이를 통해 "인터페이스를 받고, 구조체를 반환한다"는 원칙이 가능합니다. 즉, 구현 측이 아닌 호출 측에서 좁은 인터페이스를 정의합니다. 그 결과 낮은 결합도와 높은 테스트 용이성이 달성됩니다.
면접관은 후보자에게 구체적인 의존성을 인터페이스로 리팩토링하도록 자주 요청합니다. 테스트 포인트는 필요한 최소한의 메서드 세트를 식별하고, 구현 측이 아닌 소비자 측에서 인터페이스를 추출할 수 있는지 여부입니다.
Q: 채널 기반 Observer와 콜백 기반 Observer의 차이점은 무엇인가?
채널은 발행자와 구독자를 시간과 공간 모두에서 분리합니다. 발행자는 구독자 함수에 대한 참조를 보유하지 않고 채널에 전송만 합니다. 구독자는 버퍼링된 채널을 사용하여 자신의 속도로 이벤트를 처리할 수 있습니다. select문을 통해 타임아웃 처리, context를 통한 취소, 여러 이벤트 소스의 멀티플렉싱이 가능합니다. 이러한 기능은 콜백으로는 쉽게 얻을 수 없습니다.
구독자 채널을 닫는 것을 잊으면 고루틴 누수가 발생합니다. 모든 Subscribe에는 채널을 닫고 버스에서 제거하는 대응하는 Unsubscribe가 있어야 합니다.
Go 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
결론
- Functional Options 패턴은 Builder와 설정 구조체를 기존 호출 코드를 깨뜨리지 않는 깔끔하고 확장 가능한 API로 대체합니다
- Go의 Strategy 패턴은 인터페이스 기반 다형성에 대응합니다. 인터페이스 정의는 구현 측이 아닌 소비자 측에서 수행합니다
- 팩토리 함수는 제로 값 초기화로는 보장할 수 없는 불변 조건을 강제합니다. 런타임 입력에는 에러를 반환하고, 패닉은 프로그래머 버그에만 사용합니다
- 채널 기반 Observer는 고루틴과 select를 활용하여 콜백 체인 없이 동시적이고 느슨하게 결합된 이벤트 전달을 구현합니다
- HTTP 미들웨어 체인은
func(http.Handler) http.Handler시그니처를 통해 구성되며, 표준 라이브러리와 서드파티 라우터에서 동일합니다 - 구조체 임베딩은 위임을 제공하는 것이지 상속이 아닙니다. 이 구분을 이해하는 것이 Go를 깊이 이해하는 개발자와 Java 패턴을 그대로 옮기는 개발자를 구분합니다
- 면접에서의 성공은 GoF 정의의 암기가 아니라 패턴을 Go의 타입 시스템에 관용적으로 적용하는 능력에 달려 있습니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
Go 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 5월 28일 업데이트
태그
공유
관련 기사

Go 에러 핸들링 2026: 패턴, 래핑, 기술 면접 핵심 질문
Go 에러 핸들링 패턴 완벽 가이드. 센티넬 에러, 커스텀 타입, errors.Is와 errors.As, %w를 활용한 에러 래핑, 기술 면접 빈출 질문까지 체계적으로 다룬다.

Go Context 패키지 2026 완벽 가이드: 취소, 타임아웃, 면접 질문
Go 언어의 context 패키지를 상세히 해설합니다. 취소 처리, 타임아웃 설정, 요청 범위 값 활용법부터 기술 면접에서 자주 나오는 질문과 답변까지 실용적인 코드 예제와 함께 설명합니다.

2026년 Go와 gRPC: 고성능 마이크로서비스 구축과 면접 핵심 질문
Go와 gRPC를 활용한 고성능 마이크로서비스 구축을 다룹니다. Protocol Buffers, 단항 및 스트리밍 RPC, 인터셉터 체인, 프로덕션 패턴, 그리고 백엔드 엔지니어 면접에서 빈출되는 질문까지 실무 중심으로 해설합니다.