PHpullh

GO · 심층 가이드

Go 패턴 완전 정리

상속도 애노테이션도 없는 Go에서 설정 주입, 미들웨어 체인, 워커 풀, 그레이스풀 셧다운을 어떤 코드 모양으로 조립하는지 20개 주제로 정리했습니다.

주제 20개 · 예제 코드 포함 · 최종 수정 2026-08-30 · 작성 pullh 편집팀

Go에는 클래스 상속이 없고, 애노테이션도 없고, 예외도 없습니다. 그래서 다른 언어에서 넘어온 사람이 가장 먼저 부딪히는 벽은 패턴을 담을 그릇이 함수와 작은 인터페이스뿐이라는 점입니다. 인터페이스는 implements 선언 없이 메서드 시그니처가 맞으면 자동으로 만족되기 때문에, 설계 순서가 거꾸로입니다. 먼저 큰 인터페이스를 정의하고 구현체를 채우는 방식은 Go에서 거의 항상 실패하고, 구체 타입을 먼저 쓰다가 소비하는 쪽에서 필요한 최소 메서드만 추려 인터페이스를 뽑아내는 순서가 관용적입니다.

읽는 순서를 제안하면, Functional Options 패턴으로 생성자 시그니처를 깨지 않고 설정을 확장하는 감각을 먼저 익히고, 미들웨어 패턴 (HTTP)핸들러 패턴, 라우터 패턴을 묶어서 보는 편이 좋습니다. 셋 다 결국 http.Handler 하나를 감싸 되돌려주는 같은 형태이고, 그 뿌리에 io.Reader와 io.Writer 인터페이스가 있습니다. 서버 골격이 잡히면 그레이스풀 셧다운으로 운영 관점을 붙이면 됩니다.

실제로 자주 터지는 곳은 종료 경로입니다. http.Server.Shutdown은 유휴 커넥션과 진행 중인 요청은 기다려 주지만, WebSocket처럼 Hijack으로 가져간 커넥션은 관리 대상에서 빠지므로 직접 추적해서 끊지 않으면 컨텍스트 타임아웃까지 그대로 매달려 있습니다. Worker Pool도 마찬가지로, 결과 채널을 닫지 않거나 소비자가 먼저 이탈하면 송신 측 고루틴이 영영 블로킹됩니다. 옵션 함수는 값 검증에 실패할 수 있으니 func(*T) error 형태로 두는 편이 나중에 후회가 적습니다.

01Functional Options 패턴

Go에서 선택적 설정을 우아하게 처리하는 대표적인 패턴입니다.

Go code

package main

import (
	"fmt"
	"time"
)

type Server struct {
	host    string
	port    int
	timeout time.Duration
	maxConn int
}

// Option 타입 — 함수로 설정 주입
type Option func(*Server)

func WithHost(host string) Option {
	return func(s *Server) { s.host = host }
}

func WithPort(port int) Option {
	return func(s *Server) { s.port = port }
}

func WithTimeout(d time.Duration) Option {
	return func(s *Server) { s.timeout = d }
}

func WithMaxConn(n int) Option {
	return func(s *Server) { s.maxConn = n }
}

// 기본값 + 옵션 오버라이드
func NewServer(opts ...Option) *Server {
	s := &Server{
		host:    "localhost",
		port:    8080,
		timeout: 30 * time.Second,
		maxConn: 100,
	}
	for _, opt := range opts { opt(s) }
	return s
}

func (s *Server) String() string {
	return fmt.Sprintf("Server{%s:%d, timeout:%v, maxConn:%d}",
		s.host, s.port, s.timeout, s.maxConn)
}

func main() {
	// 기본값만 사용
	s1 := NewServer()
	fmt.Println(s1)

	// 일부만 오버라이드
	s2 := NewServer(
		WithHost("0.0.0.0"),
		WithPort(9090),
		WithTimeout(60*time.Second),
	)
	fmt.Println(s2)

	// 모두 오버라이드
	s3 := NewServer(
		WithHost("api.example.com"),
		WithPort(443),
		WithMaxConn(1000),
	)
	fmt.Println(s3)
}
알아두면 좋은 점

Functional Options 패턴은 하위 호환성을 유지하면서 새 옵션을 추가하기 쉽습니다. 라이브러리 API 설계에 특히 유용합니다.

자주 하는 실수

구조체에 공개 필드를 그냥 두면 제약 조건을 걸기 어렵습니다. Functional Options로 유효성 검사를 Option 함수 안에 넣을 수 있습니다.

02Worker Pool 패턴

고루틴 풀로 동시성을 제어하고 리소스를 효율적으로 사용합니다.

Go code

package main

import (
	"fmt"
	"sync"
	"time"
)

type Job struct {
	ID    int
	Input string
}

type Result struct {
	JobID  int
	Output string
	Err    error
}

func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done()
	for job := range jobs {
		// 작업 처리 시뮬레이션
		time.Sleep(100 * time.Millisecond)
		results <- Result{
			JobID:  job.ID,
			Output: fmt.Sprintf("[Worker%d] %s 처리완료", id, job.Input),
		}
	}
}

func WorkerPool(numWorkers, numJobs int) []Result {
	jobs    := make(chan Job, numJobs)
	results := make(chan Result, numJobs)

	var wg sync.WaitGroup

	// Worker 시작
	for i := 1; i <= numWorkers; i++ {
		wg.Add(1)
		go worker(i, jobs, results, &wg)
	}

	// Job 전송
	for i := 1; i <= numJobs; i++ {
		jobs <- Job{ID: i, Input: fmt.Sprintf("task-%d", i)}
	}
	close(jobs) // 전송 완료

	// 모든 워커 완료 대기 후 results 채널 닫기
	go func() {
		wg.Wait()
		close(results)
	}()

	// 결과 수집
	var all []Result
	for r := range results {
		all = append(all, r)
	}
	return all
}

func main() {
	start := time.Now()
	results := WorkerPool(3, 9)
	fmt.Printf("완료: %d개, 소요: %v
",
		len(results), time.Since(start))
	for _, r := range results {
		fmt.Println(r.Output)
	}
}
알아두면 좋은 점

Worker Pool에서 워커 수를 CPU 코어 수에 맞추면 CPU 바운드 작업에 최적입니다. I/O 바운드 작업은 더 많은 워커가 효과적입니다.

자주 하는 실수

results 채널을 닫지 않으면 for range results가 영원히 블록됩니다. 반드시 모든 워커가 완료된 후 채널을 닫으세요.

03io.Reader &amp; io.Writer 인터페이스

Go 표준 라이브러리의 가장 강력한 인터페이스. 파이프라인과 데코레이터 패턴의 기반.

Go code

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"strings"
)

// 커스텀 io.Reader 구현
type UpperReader struct {
	r io.Reader
}

func (u *UpperReader) Read(p []byte) (int, error) {
	n, err := u.r.Read(p)
	for i := 0; i < n; i++ {
		if p[i] >= 'a' && p[i] <= 'z' {
			p[i] -= 32
		}
	}
	return n, err
}

// 커스텀 io.Writer 구현
type CountWriter struct {
	w    io.Writer
	n    int64
}

func (c *CountWriter) Write(p []byte) (int, error) {
	n, err := c.w.Write(p)
	c.n += int64(n)
	return n, err
}

// io.TeeReader — 읽으면서 동시에 쓰기
func teeExample() {
	r := strings.NewReader("Hello, Go!")
	var buf bytes.Buffer
	tee := io.TeeReader(r, &buf)

	data, _ := io.ReadAll(tee)
	fmt.Println("읽음:", string(data))
	fmt.Println("복사:", buf.String())
}

// io.MultiWriter — 여러 Writer에 동시 쓰기
func multiWriterExample() {
	var b1, b2 bytes.Buffer
	mw := io.MultiWriter(&b1, &b2)
	fmt.Fprintln(mw, "동시에 두 곳에 쓰기")
	fmt.Println(b1.String() == b2.String()) // true
}

func main() {
	// UpperReader 데코레이터
	r := strings.NewReader("hello world")
	upper := &UpperReader{r: r}
	buf := make([]byte, 64)
	n, _ := upper.Read(buf)
	fmt.Println(string(buf[:n])) // HELLO WORLD

	// CountWriter
	var buf2 bytes.Buffer
	cw := &CountWriter{w: &buf2}
	fmt.Fprintln(cw, "bytes counted")
	fmt.Println("쓴 바이트:", cw.n)

	teeExample()
	multiWriterExample()

	// bufio로 성능 향상
	r2 := strings.NewReader("line1
line2
line3")
	scanner := bufio.NewScanner(r2)
	for scanner.Scan() { fmt.Println(scanner.Text()) }
}
알아두면 좋은 점

io.Readerio.Writer로 함수를 설계하면 테스트 시 strings.NewReaderbytes.Buffer로 쉽게 대체할 수 있습니다.

자주 하는 실수

Read()n > 0이면서 동시에 err != nil을 반환할 수 있습니다. n > 0인 데이터를 먼저 처리한 후 에러를 확인하세요.

04cmp 패키지 (Go 1.21+)

cmp.Compare, cmp.Ordered 제네릭 비교 유틸

Go code

<span class="cm">// cmp 패키지 (Go 1.21+) 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("cmp 패키지 (Go 1.21+)") }
알아두면 좋은 점

GO 공식 문서를 함께 참고하세요.

자주 하는 실수

자주 발생하는 실수에 주의하세요.

05리플렉션 기초 — reflect 패키지

reflect 패키지는 런타임에 타입 정보를 조회하고 값을 동적으로 조작합니다. ORM, JSON 직렬화, DI 프레임워크 등 범용 라이브러리 개발에 필수적이지만, 성능과 타입 안전성을 트레이드오프합니다.

Go code

package main

import (
	"fmt"
	"reflect"
	"strings"
)

type User struct {
	Name  string  // json:"name" validate:"required"
	Email string  // json:"email" validate:"required,email"
	Age   int     // json:"age" validate:"min=0,max=150"
}
// 참고: 실제 코드에서는 백틱 태그를 사용합니다
// Name string `json:"name" validate:"required"`

// 1. 타입 정보 조회
func inspectType(v interface{}) {
	t := reflect.TypeOf(v)
	fmt.Printf("타입: %s, 종류: %s\n", t.Name(), t.Kind())

	if t.Kind() == reflect.Struct {
		for i := 0; i < t.NumField(); i++ {
			f := t.Field(i)
			tag := f.Tag.Get("json")
			validate := f.Tag.Get("validate")
			fmt.Printf("  필드: %-6s 타입: %-6s json:%s validate:%s\n",
				f.Name, f.Type, tag, validate)
		}
	}
}

// 2. 동적 값 조작
func setField(obj interface{}, fieldName string, value interface{}) error {
	v := reflect.ValueOf(obj)
	if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("포인터가 필요합니다")
	}
	field := v.Elem().FieldByName(fieldName)
	if !field.IsValid() {
		return fmt.Errorf("필드 '%s'를 찾을 수 없습니다", fieldName)
	}
	if !field.CanSet() {
		return fmt.Errorf("필드 '%s'를 설정할 수 없습니다", fieldName)
	}
	val := reflect.ValueOf(value)
	if field.Type() != val.Type() {
		return fmt.Errorf("타입 불일치: %s != %s", field.Type(), val.Type())
	}
	field.Set(val)
	return nil
}

// 3. 간단한 구조체 → Map 변환 (JSON 태그 기반)
func structToMap(v interface{}) map[string]interface{} {
	result := make(map[string]interface{})
	val := reflect.ValueOf(v)
	typ := reflect.TypeOf(v)

	for i := 0; i < val.NumField(); i++ {
		field := typ.Field(i)
		jsonTag := field.Tag.Get("json")
		if jsonTag == "" || jsonTag == "-" {
			continue
		}
		key := strings.Split(jsonTag, ",")[0]
		result[key] = val.Field(i).Interface()
	}
	return result
}

func main() {
	user := User{Name: "홍길동", Email: "hong@go.dev", Age: 30}

	// 타입 조회
	fmt.Println("=== 타입 정보 ===")
	inspectType(user)

	// 동적 값 설정
	fmt.Println("\n=== 동적 값 변경 ===")
	fmt.Printf("변경 전: %+v\n", user)
	setField(&user, "Name", "김철수")
	setField(&user, "Age", 25)
	fmt.Printf("변경 후: %+v\n", user)

	// 구조체 → Map
	fmt.Println("\n=== Map 변환 ===")
	m := structToMap(user)
	for k, v := range m {
		fmt.Printf("  %s: %v\n", k, v)
	}
}
알아두면 좋은 점

리플렉션은 일반 코드보다 10~100배 느립니다. 핫 패스(hot path)에서는 피하고, 초기화 시점에 리플렉션 결과를 캐싱하거나 코드 생성(go generate)을 대안으로 고려하세요.

자주 하는 실수

미내보내 필드(소문자)는 reflect.Value.Set()으로 변경할 수 없습니다. CanSet()으로 먼저 확인하지 않으면 패닉이 발생합니다.

06클린 아키텍처

계층 분리로 테스트 가능하고 유지보수하기 좋은 구조를 만듭니다. 도메인 → 유스케이스 → 인프라 순서로 의존합니다.

Go code

package main

import "fmt"

// 도메인 계층 (의존성 없음)
type User struct {
	ID   int
	Name string
}
type UserRepository interface {
	FindByID(id int) (*User, error)
}

// 유스케이스 계층 (도메인에만 의존)
type GetUserUseCase struct {
	repo UserRepository
}
func (uc *GetUserUseCase) Execute(id int) (*User, error) {
	return uc.repo.FindByID(id)
}

// 인프라 계층 (모든 곳에 의존 가능)
type MemoryUserRepo struct{ data map[int]*User }
func (r *MemoryUserRepo) FindByID(id int) (*User, error) {
	u, ok := r.data[id]
	if !ok { return nil, fmt.Errorf("사용자 %d 없음", id) }
	return u, nil
}

// 조립 (main 또는 DI 컨테이너)
func main() {
	repo := &MemoryUserRepo{data: map[int]*User{
		1: {ID: 1, Name: "Gopher"},
	}}
	uc := &GetUserUseCase{repo: repo}
	user, _ := uc.Execute(1)
	fmt.Printf("사용자: %+v\n", *user)
}
알아두면 좋은 점

의존성 방향은 항상 안쪽(도메인)을 향해야 합니다. 도메인 계층은 어떤 외부 패키지도 import하지 않습니다.

자주 하는 실수

유스케이스 계층에서 HTTP, DB 등 인프라 세부사항에 직접 의존하면 테스트와 교체가 어렵습니다. 인터페이스로 추상화하세요.

07핸들러 패턴

HTTP 핸들러를 구조체로 구현하여 의존성을 주입합니다. 테스트하기 쉬운 구조입니다.

Go code

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type UserService interface {
	GetByID(id string) (map[string]string, error)
}

type mockService struct{}
func (s *mockService) GetByID(id string) (map[string]string, error) {
	return map[string]string{"id": id, "name": "Gopher"}, nil
}

// 핸들러 구조체
type UserHandler struct {
	svc UserService
}

func NewUserHandler(svc UserService) *UserHandler {
	return &UserHandler{svc: svc}
}

func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	user, err := h.svc.GetByID(id)
	if err != nil {
		http.Error(w, err.Error(), 404)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(user)
}

func (h *UserHandler) RegisterRoutes(mux *http.ServeMux) {
	mux.HandleFunc("GET /users/{id}", h.Get)
}

func main() {
	svc := &mockService{}
	handler := NewUserHandler(svc)

	mux := http.NewServeMux()
	handler.RegisterRoutes(mux)
	fmt.Println("핸들러 등록 완료: GET /users/{id}")
}
알아두면 좋은 점

핸들러 구조체에 서비스 인터페이스를 주입하면 테스트 시 목 서비스로 교체할 수 있습니다.

자주 하는 실수

핸들러에서 비즈니스 로직을 직접 구현하면 테스트와 재사용이 어렵습니다. 서비스 계층으로 분리하세요.

08라우터 패턴

커스텀 라우터로 경로 매칭, 매개변수 추출, 메서드 기반 분기를 구현합니다.

Go code

package main

import (
	"fmt"
	"net/http"
	"strings"
)

type Router struct {
	routes map[string]map[string]http.HandlerFunc
}

func NewRouter() *Router {
	return &Router{routes: make(map[string]map[string]http.HandlerFunc)}
}

func (r *Router) Handle(method, path string, h http.HandlerFunc) {
	if r.routes[path] == nil {
		r.routes[path] = make(map[string]http.HandlerFunc)
	}
	r.routes[path][method] = h
}

func (r *Router) GET(path string, h http.HandlerFunc)  { r.Handle("GET", path, h) }
func (r *Router) POST(path string, h http.HandlerFunc) { r.Handle("POST", path, h) }

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	path := req.URL.Path
	if handlers, ok := r.routes[path]; ok {
		if h, ok := handlers[req.Method]; ok {
			h(w, req)
			return
		}
		allow := strings.Join(keys(handlers), ", ")
		w.Header().Set("Allow", allow)
		http.Error(w, "Method Not Allowed", 405)
		return
	}
	http.NotFound(w, req)
}

func keys(m map[string]http.HandlerFunc) []string {
	k := make([]string, 0, len(m))
	for key := range m { k = append(k, key) }
	return k
}

func main() {
	r := NewRouter()
	r.GET("/api/users", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "사용자 목록")
	})
	r.POST("/api/users", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(w, "사용자 생성")
	})
	fmt.Println("라우터 등록 완료")
}
알아두면 좋은 점

Go 1.22+에서는 http.ServeMux가 메서드 + 경로 매칭을 지원하므로 별도 라우터가 불필요할 수 있습니다.

자주 하는 실수

커스텀 라우터에서 경로 매칭 순서를 잘못 구현하면 더 구체적인 경로가 무시될 수 있습니다.

09그레이스풀 셧다운

서버가 종료 신호를 받으면 진행 중인 요청을 완료하고 안전하게 종료합니다.

Go code

package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(2 * time.Second) // 느린 요청 시뮬레이션
		fmt.Fprintln(w, "완료")
	})

	srv := &http.Server{Addr: ":8080", Handler: mux}

	// 종료 신호 수신
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		fmt.Println("서버 시작 :8080")
		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			fmt.Printf("서버 에러: %v\n", err)
		}
	}()

	<-quit
	fmt.Println("종료 신호 수신...")

	// 5초 안에 진행 중인 요청 완료
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		fmt.Printf("강제 종료: %v\n", err)
	}
	fmt.Println("서버 안전 종료 완료")
}
알아두면 좋은 점

srv.Shutdown은 새 연결을 거부하면서 기존 연결이 완료될 때까지 대기합니다. 타임아웃을 설정하여 무한 대기를 방지하세요.

자주 하는 실수

os.Exit(0)으로 즉시 종료하면 진행 중인 요청이 끊어지고 DB 트랜잭션이 롤백되지 않을 수 있습니다.

10헬스 체크 패턴

서버의 건강 상태를 점검하는 엔드포인트. Kubernetes liveness/readiness probe에 사용됩니다.

Go code

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"
)

type HealthChecker struct {
	mu     sync.RWMutex
	checks map[string]func() error
}

func NewHealthChecker() *HealthChecker {
	return &HealthChecker{checks: make(map[string]func() error)}
}

func (h *HealthChecker) Register(name string, check func() error) {
	h.mu.Lock()
	defer h.mu.Unlock()
	h.checks[name] = check
}

func (h *HealthChecker) Handler(w http.ResponseWriter, r *http.Request) {
	h.mu.RLock()
	defer h.mu.RUnlock()

	status := "ok"
	results := map[string]string{}
	for name, check := range h.checks {
		if err := check(); err != nil {
			status = "degraded"
			results[name] = err.Error()
		} else {
			results[name] = "ok"
		}
	}

	code := 200
	if status != "ok" { code = 503 }
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(map[string]any{
		"status": status, "checks": results,
		"time": time.Now().Format(time.RFC3339),
	})
}

func main() {
	hc := NewHealthChecker()
	hc.Register("db", func() error { return nil })
	hc.Register("cache", func() error { return fmt.Errorf("연결 끊김") })

	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", hc.Handler)
	fmt.Println("헬스 체크: GET /health")
}
알아두면 좋은 점

liveness probe는 간단하게(프로세스 생존), readiness probe는 의존성(DB, 캐시)까지 확인하세요.

자주 하는 실수

헬스 체크에서 무거운 쿼리를 실행하면 체크 자체가 서버 성능을 저하시킵니다. 간단한 ping/pong으로 충분합니다.

11설정 관리 패턴

환경 변수, 파일, 기본값을 우선순위에 따라 병합하는 설정 관리 패턴입니다.

Go code

package main

import (
	"fmt"
	"os"
	"strconv"
)

type Config struct {
	Host     string
	Port     int
	Debug    bool
	DBSource string
}

func LoadConfig() *Config {
	cfg := &Config{
		Host:     "localhost",
		Port:     8080,
		Debug:    false,
		DBSource: "postgres://localhost/app",
	}

	// 환경 변수로 오버라이드
	if v := os.Getenv("APP_HOST"); v != "" { cfg.Host = v }
	if v := os.Getenv("APP_PORT"); v != "" {
		if p, err := strconv.Atoi(v); err == nil { cfg.Port = p }
	}
	if v := os.Getenv("APP_DEBUG"); v == "true" { cfg.Debug = true }
	if v := os.Getenv("DATABASE_URL"); v != "" { cfg.DBSource = v }

	return cfg
}

func (c *Config) Validate() error {
	if c.Port < 1 || c.Port > 65535 {
		return fmt.Errorf("유효하지 않은 포트: %d", c.Port)
	}
	if c.DBSource == "" {
		return fmt.Errorf("데이터베이스 URL 필수")
	}
	return nil
}

func main() {
	cfg := LoadConfig()
	if err := cfg.Validate(); err != nil {
		fmt.Println("설정 에러:", err)
		return
	}
	fmt.Printf("설정: %+v\n", *cfg)
}
알아두면 좋은 점

설정 우선순위: 환경 변수 > 설정 파일 > 기본값. 12-Factor App 원칙에 따라 환경 변수를 최우선으로 사용하세요.

자주 하는 실수

비밀 정보(DB 비밀번호, API 키)를 코드나 설정 파일에 하드코딩하지 마세요. 환경 변수나 시크릿 매니저를 사용하세요.

12CLI 도구 패턴

flag 패키지와 서브커맨드 패턴으로 CLI 도구를 구현합니다.

Go code

package main

import (
	"flag"
	"fmt"
	"os"
)

type Command struct {
	Name string
	Desc string
	Run  func(args []string) error
}

func main() {
	commands := map[string]*Command{
		"serve": {Name: "serve", Desc: "서버 시작",
			Run: func(args []string) error {
				fs := flag.NewFlagSet("serve", flag.ExitOnError)
				port := fs.Int("port", 8080, "포트 번호")
				fs.Parse(args)
				fmt.Printf("서버 시작: :%d\n", *port)
				return nil
			}},
		"migrate": {Name: "migrate", Desc: "DB 마이그레이션",
			Run: func(args []string) error {
				fs := flag.NewFlagSet("migrate", flag.ExitOnError)
				dir := fs.String("dir", "up", "방향 (up/down)")
				fs.Parse(args)
				fmt.Printf("마이그레이션: %s\n", *dir)
				return nil
			}},
	}

	if len(os.Args) < 2 {
		fmt.Println("사용법: app <command> [options]")
		for _, cmd := range commands {
			fmt.Printf("  %-10s %s\n", cmd.Name, cmd.Desc)
		}
		os.Exit(1)
	}

	cmd, ok := commands[os.Args[1]]
	if !ok {
		fmt.Fprintf(os.Stderr, "알 수 없는 명령: %s\n", os.Args[1])
		os.Exit(1)
	}
	if err := cmd.Run(os.Args[2:]); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
알아두면 좋은 점

실무에서는 cobra 라이브러리가 서브커맨드, 자동완성, 도움말 생성 등을 제공합니다.

자주 하는 실수

os.Exitdefer를 실행하지 않습니다. 정리 작업이 필요하면 os.Exit 전에 수행하세요.

13워커 큐 패턴

백그라운드 작업 처리를 위한 워커 큐. 작업 제출, 재시도, 동시성 제한을 구현합니다.

Go code

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

type Job struct {
	ID      int
	Payload string
}

type WorkerQueue struct {
	jobs    chan Job
	results chan string
	wg      sync.WaitGroup
}

func NewWorkerQueue(workers, queueSize int) *WorkerQueue {
	wq := &WorkerQueue{
		jobs:    make(chan Job, queueSize),
		results: make(chan string, queueSize),
	}
	for i := 0; i < workers; i++ {
		wq.wg.Add(1)
		go wq.worker(i)
	}
	return wq
}

func (wq *WorkerQueue) worker(id int) {
	defer wq.wg.Done()
	for job := range wq.jobs {
		time.Sleep(100 * time.Millisecond)
		wq.results <- fmt.Sprintf("워커%d: 작업%d(%s) 완료",
			id, job.ID, job.Payload)
	}
}

func (wq *WorkerQueue) Submit(ctx context.Context, job Job) error {
	select {
	case wq.jobs <- job:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

func (wq *WorkerQueue) Close() {
	close(wq.jobs)
	wq.wg.Wait()
	close(wq.results)
}

func main() {
	wq := NewWorkerQueue(3, 10)
	ctx := context.Background()
	for i := 1; i <= 5; i++ {
		wq.Submit(ctx, Job{ID: i, Payload: fmt.Sprintf("데이터-%d", i)})
	}
	go func() { wq.Close() }()
	for r := range wq.results { fmt.Println(r) }
}
알아두면 좋은 점

큐 크기(버퍼)를 워커 수의 2-3배로 설정하면 작업 제출과 처리 사이의 속도 차이를 완화합니다.

자주 하는 실수

큐가 가득 차면 Submit이 블로킹됩니다. context로 타임아웃을 설정하여 무한 대기를 방지하세요.

14이벤트 시스템

Pub/Sub 패턴으로 컴포넌트 간 느슨한 결합을 구현합니다. 이벤트 발행과 구독을 분리합니다.

Go code

package main

import (
	"fmt"
	"sync"
)

type Event struct {
	Type string
	Data any
}

type Handler func(Event)

type EventBus struct {
	mu       sync.RWMutex
	handlers map[string][]Handler
}

func NewEventBus() *EventBus {
	return &EventBus{handlers: make(map[string][]Handler)}
}

func (eb *EventBus) Subscribe(eventType string, h Handler) {
	eb.mu.Lock()
	defer eb.mu.Unlock()
	eb.handlers[eventType] = append(eb.handlers[eventType], h)
}

func (eb *EventBus) Publish(evt Event) {
	eb.mu.RLock()
	defer eb.mu.RUnlock()
	for _, h := range eb.handlers[evt.Type] {
		go h(evt) // 비동기 처리
	}
}

func main() {
	bus := NewEventBus()

	bus.Subscribe("user.created", func(e Event) {
		fmt.Println("이메일 발송:", e.Data)
	})
	bus.Subscribe("user.created", func(e Event) {
		fmt.Println("로그 기록:", e.Data)
	})
	bus.Subscribe("order.placed", func(e Event) {
		fmt.Println("재고 확인:", e.Data)
	})

	bus.Publish(Event{Type: "user.created", Data: "Gopher"})
	bus.Publish(Event{Type: "order.placed", Data: "주문#100"})

	// 비동기 핸들러 완료 대기
	fmt.Scanln()
}
알아두면 좋은 점

이벤트 핸들러를 고루틴으로 실행하면 발행자가 블로킹되지 않습니다. 에러 처리는 핸들러 내부에서 수행하세요.

자주 하는 실수

이벤트 핸들러에서 패닉이 발생하면 해당 고루틴만 종료됩니다. 프로덕션에서는 recover로 보호하세요.

15플러그인 시스템

인터페이스 기반 플러그인 시스템으로 확장 가능한 아키텍처를 구현합니다.

Go code

package main

import "fmt"

// 플러그인 인터페이스
type Plugin interface {
	Name() string
	Init() error
	Execute(data map[string]any) (map[string]any, error)
}

// 플러그인 레지스트리
type Registry struct {
	plugins map[string]Plugin
}

func NewRegistry() *Registry {
	return &Registry{plugins: make(map[string]Plugin)}
}

func (r *Registry) Register(p Plugin) error {
	if err := p.Init(); err != nil {
		return fmt.Errorf("플러그인 %s 초기화 실패: %w", p.Name(), err)
	}
	r.plugins[p.Name()] = p
	return nil
}

func (r *Registry) Execute(name string, data map[string]any) (map[string]any, error) {
	p, ok := r.plugins[name]
	if !ok { return nil, fmt.Errorf("플러그인 없음: %s", name) }
	return p.Execute(data)
}

// 플러그인 구현
type LogPlugin struct{}
func (p *LogPlugin) Name() string { return "logger" }
func (p *LogPlugin) Init() error  { fmt.Println("로거 초기화"); return nil }
func (p *LogPlugin) Execute(data map[string]any) (map[string]any, error) {
	fmt.Printf("[LOG] %v\n", data)
	return data, nil
}

type TransformPlugin struct{}
func (p *TransformPlugin) Name() string { return "transform" }
func (p *TransformPlugin) Init() error  { return nil }
func (p *TransformPlugin) Execute(data map[string]any) (map[string]any, error) {
	data["transformed"] = true
	return data, nil
}

func main() {
	reg := NewRegistry()
	reg.Register(&LogPlugin{})
	reg.Register(&TransformPlugin{})

	result, _ := reg.Execute("transform", map[string]any{"key": "value"})
	reg.Execute("logger", result)
}
알아두면 좋은 점

플러그인 인터페이스를 작게 유지하면 새 플러그인을 쉽게 추가할 수 있습니다. Init()Execute()만으로 충분합니다.

자주 하는 실수

플러그인 간 의존성을 관리하지 않으면 초기화 순서 문제가 발생합니다. 의존성 그래프를 구축하여 순서를 보장하세요.

16마이크로서비스 패턴

Go 마이크로서비스의 기본 구조. 서비스 디스커버리, 헬스 체크, 설정을 포함합니다.

Go code

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

// 서비스 인터페이스
type Service interface {
	Name() string
	Start(ctx context.Context) error
	Health() error
}

// 기본 마이크로서비스 구조
type Microservice struct {
	name   string
	server *http.Server
	mux    *http.ServeMux
}

func NewMicroservice(name string, port int) *Microservice {
	mux := http.NewServeMux()
	ms := &Microservice{
		name: name,
		mux:  mux,
		server: &http.Server{
			Addr:         fmt.Sprintf(":%d", port),
			Handler:      mux,
			ReadTimeout:  5 * time.Second,
			WriteTimeout: 10 * time.Second,
		},
	}

	// 기본 엔드포인트
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode(map[string]string{
			"service": name, "status": "healthy",
			"time": time.Now().Format(time.RFC3339),
		})
	})
	mux.HandleFunc("GET /info", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode(map[string]string{
			"name": name, "version": "1.0.0",
		})
	})
	return ms
}

func (ms *Microservice) Name() string { return ms.name }
func (ms *Microservice) Health() error { return nil }
func (ms *Microservice) HandleFunc(pattern string, h http.HandlerFunc) {
	ms.mux.HandleFunc(pattern, h)
}

func main() {
	svc := NewMicroservice("user-service", 8080)
	svc.HandleFunc("GET /api/users", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode([]map[string]string{{"name": "Gopher"}})
	})
	fmt.Printf("서비스 '%s' 준비 완료\n", svc.Name())
}
알아두면 좋은 점

모든 마이크로서비스에 /health/info 엔드포인트를 표준으로 제공하세요. 모니터링과 디버깅에 필수적입니다.

자주 하는 실수

서비스 간 동기 호출이 많으면 결합도가 높아지고 장애가 전파됩니다. 이벤트 기반 비동기 통신을 고려하세요.

17gRPC 기초

Protocol Buffers와 gRPC로 고성능 서비스 간 통신을 구현합니다.

Go code

package main

import "fmt"

// protobuf 정의 (user.proto):
// syntax = "proto3";
// package user;
// option go_package = "./pb";
//
// service UserService {
//   rpc GetUser (GetUserRequest) returns (User);
//   rpc ListUsers (ListUsersRequest) returns (stream User);
// }
//
// message GetUserRequest { int32 id = 1; }
// message ListUsersRequest { int32 page = 1; int32 size = 2; }
// message User {
//   int32 id = 1;
//   string name = 2;
//   string email = 3;
// }

// gRPC 서버 구현 예시:
// type server struct {
//     pb.UnimplementedUserServiceServer
// }
// func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
//     return &pb.User{Id: req.Id, Name: "Gopher"}, nil
// }

func main() {
	fmt.Println("gRPC 워크플로우:")
	fmt.Println("1. .proto 파일 정의")
	fmt.Println("2. protoc --go_out=. --go-grpc_out=. user.proto")
	fmt.Println("3. 서버: pb.RegisterUserServiceServer(grpcServer, &server{})")
	fmt.Println("4. 클라이언트: pb.NewUserServiceClient(conn)")
	fmt.Println("5. grpc.NewServer() → Serve(listener)")
}
알아두면 좋은 점

gRPC는 HTTP/2 기반으로 양방향 스트리밍, 바이너리 직렬화로 REST보다 빠릅니다. 서비스 간 내부 통신에 적합합니다.

자주 하는 실수

UnimplementedXxxServer를 임베딩하지 않으면 새 RPC 메서드 추가 시 컴파일 에러가 발생합니다. 항상 임베딩하세요.

18프로토콜 버퍼 패턴

Protocol Buffers의 Go 사용 패턴. 메시지 정의, 직렬화, 검증을 다룹니다.

Go code

package main

import (
	"encoding/json"
	"fmt"
)

// protobuf가 생성하는 Go 코드와 유사한 구조
type UserProto struct {
	ID    int32  `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

// 검증 메서드 (protoc-gen-validate 스타일)
func (u *UserProto) Validate() error {
	if u.Name == "" {
		return fmt.Errorf("name은 필수입니다")
	}
	if len(u.Name) > 100 {
		return fmt.Errorf("name은 100자 이하여야 합니다")
	}
	return nil
}

// JSON ↔ Proto 변환
func (u *UserProto) ToJSON() ([]byte, error) {
	return json.Marshal(u)
}

func UserFromJSON(data []byte) (*UserProto, error) {
	var u UserProto
	if err := json.Unmarshal(data, &u); err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return &u, nil
}

func main() {
	user := &UserProto{ID: 1, Name: "Gopher", Email: "go@dev"}
	data, _ := user.ToJSON()
	fmt.Println(string(data))

	u2, err := UserFromJSON([]byte(`{"id":2,"name":"","email":"test"}`))
	if err != nil {
		fmt.Println("에러:", err) // name은 필수입니다
	}
	_ = u2
}
알아두면 좋은 점

buf 도구를 사용하면 protobuf 빌드, 린팅, 호환성 검사를 자동화할 수 있습니다.

자주 하는 실수

protobuf 필드 번호를 변경하면 이전 버전과 호환되지 않습니다. 필드를 삭제할 때는 번호를 reserved로 표시하세요.

19API 게이트웨이 패턴

여러 마이크로서비스에 대한 단일 진입점. 라우팅, 인증, 속도 제한을 처리합니다.

Go code

package main

import (
	"fmt"
	"net/http"
	"net/http/httputil"
	"net/url"
)

type Route struct {
	Path   string
	Target string
}

type Gateway struct {
	routes []Route
	mux    *http.ServeMux
}

func NewGateway(routes []Route) *Gateway {
	gw := &Gateway{routes: routes, mux: http.NewServeMux()}

	for _, route := range routes {
		target, _ := url.Parse(route.Target)
		proxy := httputil.NewSingleHostReverseProxy(target)

		path := route.Path
		gw.mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
			fmt.Printf("[GW] %s %s → %s\n", r.Method, r.URL.Path, target)
			proxy.ServeHTTP(w, r)
		})
	}
	return gw
}

func (gw *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	gw.mux.ServeHTTP(w, r)
}

func main() {
	routes := []Route{
		{Path: "/api/users/", Target: "http://localhost:8081"},
		{Path: "/api/orders/", Target: "http://localhost:8082"},
		{Path: "/api/products/", Target: "http://localhost:8083"},
	}
	gw := NewGateway(routes)
	fmt.Println("게이트웨이 라우팅:")
	for _, r := range routes {
		fmt.Printf("  %s → %s\n", r.Path, r.Target)
	}
	_ = gw
}
알아두면 좋은 점

httputil.ReverseProxy는 Go 표준 라이브러리의 리버스 프록시입니다. 커스텀 헤더 추가, 요청 수정 등이 가능합니다.

자주 하는 실수

게이트웨이에 비즈니스 로직을 넣으면 단일 장애점이 됩니다. 라우팅, 인증, 속도 제한만 담당하세요.

20미들웨어 패턴 (HTTP)

범용 HTTP 미들웨어 패턴. 요청 ID, 타임아웃, 인증을 미들웨어로 분리합니다.

Go code

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"crypto/rand"
	"encoding/hex"
)

type ctxKey string

func RequestID(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Request-ID")
		if id == "" {
			b := make([]byte, 8)
			rand.Read(b)
			id = hex.EncodeToString(b)
		}
		ctx := context.WithValue(r.Context(), ctxKey("requestID"), id)
		w.Header().Set("X-Request-ID", id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func Timeout(d time.Duration) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx, cancel := context.WithTimeout(r.Context(), d)
			defer cancel()
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func RequireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := r.Header.Get("Authorization")
		if token == "" {
			http.Error(w, "인증 필요", 401)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		reqID := r.Context().Value(ctxKey("requestID"))
		fmt.Fprintf(w, "요청 ID: %s\n", reqID)
	})

	wrapped := RequestID(Timeout(5*time.Second)(RequireAuth(handler)))
	fmt.Println("미들웨어 스택 준비:", wrapped)
}
알아두면 좋은 점

요청 ID 미들웨어는 로그 추적에 필수입니다. 모든 로그에 요청 ID를 포함하면 분산 시스템에서 요청 흐름을 추적할 수 있습니다.

자주 하는 실수

미들웨어에서 설정한 context 값을 다운스트림에서 사용하려면 r.WithContext(ctx)로 새 요청을 만들어야 합니다.

정리하며

  • 인터페이스는 제공하는 쪽이 아니라 사용하는 쪽에서, 필요한 최소 메서드만 뽑아 정의합니다
  • 미들웨어·데코레이터·파이프라인은 모두 같은 형태를 감싸 되돌려주는 한 가지 구조의 변주입니다
  • 옵션 함수는 error를 반환하게 만들어 잘못된 설정을 생성 시점에서 잡습니다
  • 셧다운 경로에서 하이재킹된 커넥션과 워커 고루틴은 따로 추적해 끊어야 누수가 없습니다

더 깊이 들어가고 싶다면 Go 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.