본문으로 건너뛰기

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

함수형 프로그래밍 패턴: 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 순서로 실행되므로 connection이 database보다 먼저 해제됩니다. 이것이 올바른 리소스 해제 순서입니다.


함수 인라이닝과 성능​

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 + 클로저: 리소스 관리와 트랜잭션을 우아하게 처리
  • 메모이제이션: 재귀 성능 문제의 실용적 해법
  • 인라이닝: 작고 단순한 함수는 자동 최적화됨