본문으로 건너뛰기

실전 고수 팁 — 함수와 클로저

함수형 프로그래밍 패턴: Map, Filter, Reduce

Go는 함수형 언어는 아니지만, 일급 함수를 활용해 map/filter/reduce 패턴을 직접 구현할 수 있습니다. Go 1.18+ 제네릭을 사용하면 타입 안전한 범용 구현이 가능합니다.

package main

import "fmt"

// 제네릭 Map — 슬라이스의 각 원소에 함수 적용
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}

// 제네릭 Filter — 조건을 만족하는 원소만 추출
func Filter[T any](slice []T, pred func(T) bool) []T {
var result []T
for _, v := range slice {
if pred(v) {
result = append(result, v)
}
}
return result
}

// 제네릭 Reduce — 슬라이스를 단일 값으로 축약
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
acc := initial
for _, v := range slice {
acc = fn(acc, v)
}
return acc
}

func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// 짝수만 필터링 → 제곱 → 합산
result := Reduce(
Map(
Filter(nums, func(n int) bool { return n%2 == 0 }),
func(n int) int { return n * n },
),
0,
func(acc, n int) int { return acc + n },
)
fmt.Println("짝수 제곱 합:", result) // 4+16+36+64+100 = 220

// 문자열 변환
words := []string{"go", "is", "fast"}
lengths := Map(words, func(s string) int { return len(s) })
fmt.Println("단어 길이:", lengths)

// 파이프라인 스타일 — 가독성을 위해 단계별 변수 분리
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
doubled := Map(evens, func(n int) int { return n * 2 })
sum := Reduce(doubled, 0, func(acc, n int) int { return acc + n })
fmt.Println("짝수 2배 합:", sum)
}

실행 결과:

짝수 제곱 합: 220
단어 길이: [2 2 4]
짝수 2배 합: 60

옵션 패턴 (Functional Options Pattern)

함수 옵션 패턴은 Go에서 선택적 파라미터를 우아하게 처리하는 표준 방법입니다. grpc-go, zap 등 유명 라이브러리에서 광범위하게 사용됩니다.

package main

import (
"fmt"
"time"
)

// 서버 설정
type Server struct {
host string
port int
timeout time.Duration
maxConns int
enableTLS bool
tlsCertFile string
}

// 옵션 타입 — 함수가 설정을 수정
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 WithMaxConns(n int) Option {
return func(s *Server) {
s.maxConns = n
}
}

func WithTLS(certFile string) Option {
return func(s *Server) {
s.enableTLS = true
s.tlsCertFile = certFile
}
}

// 기본값과 옵션을 조합해 서버 생성
func NewServer(opts ...Option) *Server {
// 기본값 설정
s := &Server{
host: "0.0.0.0",
port: 8080,
timeout: 30 * time.Second,
maxConns: 100,
}
// 옵션 적용
for _, opt := range opts {
opt(s)
}
return s
}

func (s *Server) String() string {
tls := "no"
if s.enableTLS {
tls = fmt.Sprintf("yes (cert: %s)", s.tlsCertFile)
}
return fmt.Sprintf("Server{host:%s, port:%d, timeout:%v, maxConns:%d, tls:%s}",
s.host, s.port, s.timeout, s.maxConns, tls)
}

func main() {
// 기본값으로 생성
s1 := NewServer()
fmt.Println("기본:", s1)

// 일부 옵션만 적용
s2 := NewServer(
WithPort(9090),
WithTimeout(60*time.Second),
)
fmt.Println("커스텀:", s2)

// 모든 옵션 적용
s3 := NewServer(
WithHost("localhost"),
WithPort(443),
WithTimeout(10*time.Second),
WithMaxConns(1000),
WithTLS("/etc/ssl/cert.pem"),
)
fmt.Println("풀 설정:", s3)
}

실행 결과:

기본: Server{host:0.0.0.0, port:8080, timeout:30s, maxConns:100, tls:no}
커스텀: Server{host:0.0.0.0, port:9090, timeout:1m0s, maxConns:100, tls:no}
풀 설정: Server{host:localhost, port:443, timeout:10s, maxConns:1000, tls:yes (cert: /etc/ssl/cert.pem)}

옵션 패턴의 장점은 하위 호환성입니다. 새로운 옵션을 추가해도 기존 코드를 변경하지 않아도 됩니다.


미들웨어 체인 구현 패턴

프로덕션 수준의 미들웨어 체이닝 패턴입니다.

package main

import (
"fmt"
"strings"
"time"
)

// 범용 처리 함수 타입
type Handler func(ctx map[string]any) error

// 미들웨어 타입
type MiddlewareFunc func(Handler) Handler

// 미들웨어 체인 구성
func Chain(h Handler, middlewares ...MiddlewareFunc) Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}

// 타이밍 미들웨어
func TimingMiddleware(next Handler) Handler {
return func(ctx map[string]any) error {
start := time.Now()
err := next(ctx)
ctx["duration"] = time.Since(start)
return err
}
}

// 로깅 미들웨어
func LoggingMiddleware(next Handler) Handler {
return func(ctx map[string]any) error {
fmt.Printf("[LOG] 처리 시작: %v\n", ctx["request"])
err := next(ctx)
if err != nil {
fmt.Printf("[LOG] 에러: %v\n", err)
} else {
fmt.Printf("[LOG] 완료: duration=%v\n", ctx["duration"])
}
return err
}
}

// 검증 미들웨어 (클로저로 설정 캡처)
func ValidationMiddleware(requiredFields ...string) MiddlewareFunc {
return func(next Handler) Handler {
return func(ctx map[string]any) error {
for _, field := range requiredFields {
if _, ok := ctx[field]; !ok {
return fmt.Errorf("필수 필드 누락: %s", field)
}
}
return next(ctx)
}
}
}

// 실제 처리 핸들러
func processHandler(ctx map[string]any) error {
req := ctx["request"].(string)
ctx["result"] = strings.ToUpper(req) + "_PROCESSED"
return nil
}

func main() {
handler := Chain(
processHandler,
TimingMiddleware,
LoggingMiddleware,
ValidationMiddleware("request", "user_id"),
)

// 정상 케이스
ctx := map[string]any{
"request": "hello world",
"user_id": "user-123",
}
if err := handler(ctx); err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println("결과:", ctx["result"])
}

fmt.Println()

// 필드 누락 케이스
ctx2 := map[string]any{
"request": "test",
// user_id 누락
}
if err := handler(ctx2); err != nil {
fmt.Println("검증 에러:", err)
}
}

실행 결과:

[LOG] 처리 시작: hello world
[LOG] 완료: duration=1µs
결과: HELLO WORLD_PROCESSED

검증 에러: 필수 필드 누락: user_id

defer와 함수 조합 패턴

package main

import (
"fmt"
"os"
"sync"
)

// 클린업 함수를 반환하는 패턴
func acquireResource(name string) (string, func()) {
fmt.Printf("리소스 획득: %s\n", name)
resource := name + "_handle"
cleanup := func() {
fmt.Printf("리소스 해제: %s\n", name)
}
return resource, cleanup
}

// 트랜잭션 패턴
func withTransaction(fn func() error) (err error) {
fmt.Println("트랜잭션 시작")
defer func() {
if err != nil {
fmt.Println("트랜잭션 롤백")
} else {
fmt.Println("트랜잭션 커밋")
}
}()
return fn()
}

// 잠금 획득 헬퍼
func withLock(mu *sync.Mutex, fn func()) {
mu.Lock()
defer mu.Unlock()
fn()
}

// 파일 처리 패턴
func withFile(path string, fn func(*os.File) error) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("파일 열기 실패: %w", err)
}
defer f.Close() // fn이 어떻게 종료되든 파일은 반드시 닫힘
return fn(f)
}

func main() {
// 클린업 패턴
db, cleanup := acquireResource("database")
defer cleanup()
fmt.Println("사용 중:", db)

conn, cleanup2 := acquireResource("connection")
defer cleanup2()
fmt.Println("사용 중:", conn)

// 트랜잭션 패턴
err := withTransaction(func() error {
fmt.Println(" 데이터 처리 중...")
return nil // 성공
})
fmt.Println("트랜잭션 결과:", err)

// 잠금 패턴
var mu sync.Mutex
counter := 0
for i := 0; i < 5; i++ {
withLock(&mu, func() {
counter++
})
}
fmt.Println("카운터:", counter)
}

실행 결과:

리소스 획득: database
사용 중: database_handle
리소스 획득: connection
사용 중: connection_handle
트랜잭션 시작
데이터 처리 중...
트랜잭션 커밋
트랜잭션 결과: <nil>
카운터: 5
리소스 해제: connection
리소스 해제: database

defer는 LIFO 순서로 실행되므로 connectiondatabase보다 먼저 해제됩니다. 이것이 올바른 리소스 해제 순서입니다.


함수 인라이닝과 성능

Go 컴파일러는 작은 함수를 자동으로 인라이닝합니다. 인라이닝은 함수 호출 오버헤드를 제거해 성능을 향상시킵니다.

package main

import "fmt"

// 인라이닝 친화적: 작고 단순
func add(a, b int) int { return a + b }

// 인라이닝 불가: 복잡하거나, go:noinline 지시어 사용
//
//go:noinline
func addNoInline(a, b int) int { return a + b }

// 벤치마크: go test -bench=. 로 확인
// 직접 실행 시 차이 확인이 어려울 수 있음
func main() {
// 컴파일러 결정 확인: go build -gcflags="-m" main.go
// "can inline add" 메시지가 표시되면 인라이닝 가능

sum := 0
for i := 0; i < 1000; i++ {
sum += add(i, i+1) // 인라이닝됨 — 함수 호출 없음
}
fmt.Println("합계:", sum)

fmt.Println("인라이닝 확인 명령어:")
fmt.Println(" go build -gcflags='-m' main.go")
fmt.Println(" go build -gcflags='-m=2' main.go # 더 상세한 출력")
}

재귀 vs 반복문 선택 실전 가이드

상황권장 방식이유
트리/그래프 DFS재귀 (깊이 제한 고려)코드가 자연스럽고 간결
피보나치/팩토리얼반복문 또는 메모이제이션재귀는 성능 최악
분할 정복재귀알고리즘 구조와 일치
파일 시스템 탐색filepath.WalkDir 또는 재귀표준 라이브러리 활용 우선
깊이 > 10,000반복문 + 명시적 스택스택 안전성
상호 재귀재귀 허용다른 방법이 없음

핵심 요약

  • 함수 옵션 패턴: 유연하고 확장 가능한 API 설계의 표준
  • 미들웨어 체인: 관심사 분리와 재사용성의 극대화
  • 제네릭 유틸: Go 1.18+에서 타입 안전한 함수형 유틸 구현 가능
  • defer + 클로저: 리소스 관리와 트랜잭션을 우아하게 처리
  • 메모이제이션: 재귀 성능 문제의 실용적 해법
  • 인라이닝: 작고 단순한 함수는 자동 최적화됨