Go 언어의 명확성 철학: 기본값 대신 옵션 패턴을 선택하는 이유

Go 언어의 '명시적' 설계 철학: 왜 기본값 파라미터는 없다?

Go는 출시 15년 동안 기본값 파라미터를 단 한 번도 도입하지 않았다. 이는 기술적 불편함이 아니라, 의도적인 설계 원칙이다. 다른 언어에서 흔히 볼 수 있는 간결한 문법을 거부하는 이유는 무엇인가? 본문에서는 그 깊은 배경과 실용적인 대안을 살펴본다.

1. 문제의 시작: 기본값 없는 함수 호출

Python이나 Java에서 익숙한 아래 코드와 비교해보자.

def create_user(name: str, age: int = 18, country: str = "Korea", vip: bool = False):
    return {"name": name, "age": age, "country": country, "vip": vip}

# 사용 예
create_user("Anna")                    # 모든 값이 기본값
create_user("Ben", age=25)             # 일부만 덮어씀
create_user("Chris", vip=True, country="US")

반면, 같은 기능을 Go로 구현하면:

func CreateUser(name string, age int, country string, vip bool) *User {
    return &User{
        Name:    name,
        Age:     age,
        Country: country,
        VIP:     vip,
    }
}

// 호출 시 모든 인자를 반드시 전달해야 함
user := CreateUser("Anna", 18, "Korea", false)

왜 이렇게까지 명시적으로 작성해야 하는가?

2. 디자인 철학: '암묵적 행동'을 배제하라

Go의 공동 설계자들, 특히 Rob Pike는 다음과 같이 말한다:

"기본값은 함수의 행위를 암묵적으로 정의한다. 하지만 우리는 코드의 의미가 항상 명확해야 한다고 믿는다. 어떤 값이 기본으로 설정되었는지 모른다면, 코드의 의도를 이해하기 어렵다."

또한 Ken Thompson은 강조한다:

"특성이 추가되기 위해서는 모두가 동의해야 한다. 단순히 '이거 있으면 편하겠네'라는 생각만으로는 안 된다."

3. 기본값의 위험: 감추어진 의도

실제 사례를 보자. 주문 처리 시스템의 함수다.

def process_order(user_id, amount, currency="USD", discount=0, 
                  tax_rate=0.1, express_shipping=False, gift_wrap=False,
                  priority="normal", notify=True, retry_count=3):
    # ... 처리 로직

이 함수를 호출할 때:

process_order("U123", 50000, discount=0.15, priority="urgent")

여기서 tax_rate, express_shipping, notify 등은 어떻게 되었는가? 6개월 후에는 누구도 기억하지 못한다.

기본값의 문제점

  • 암묵적 종속성: 호출자가 기본값을 몰라야 API의 진짜 의미를 알 수 없다.
  • 파라미터 폭증: 기능이 늘어날수록 파라미터 수가 급격히 증가한다.
  • 버전 호환성 문제: 기본값 변경 시 기존 코드가 예상치 못한 동작을 하게 된다.
  • 테스트 난이도 상승: 모든 조합을 다 테스트해야 하므로 테스트 케이스 수가 지수적으로 증가한다.

4. Go의 대안: 명시적 구성 방식

4.1 래퍼 함수 (Wrapper Functions)

간단한 경우에 적합하다. 기본값을 함수 이름으로 표현한다.

type User struct {
    Name    string
    Age     int
    Country string
    VIP     bool
}

func newUser(name string) *User {
    return &User{
        Name:    name,
        Age:     18,
        Country: "Korea",
        VIP:     false,
    }
}

func newVIPUser(name string, age int) *User {
    return &User{
        Name:    name,
        Age:     age,
        Country: "Korea",
        VIP:     true,
    }
}

// 사용
user1 := newUser("Dana")
user2 := newVIPUser("Eve", 27)

4.2 구성 구조체 (Config Struct)

파라미터가 많아질수록 유리하다. 구성 요소를 명확하게 그룹화할 수 있다.

type ServerConfig struct {
    Host    string
    Port    int
    Timeout time.Duration
    Debug   bool
    Logger  *log.Logger
}

func NewServer(config ServerConfig) *Server {
    if config.Host == "" {
        config.Host = "localhost"
    }
    if config.Port == 0 {
        config.Port = 8080
    }
    if config.Timeout == 0 {
        config.Timeout = 30 * time.Second
    }
    if config.Logger == nil {
        config.Logger = log.Default()
    }

    return &Server{
        host:    config.Host,
        port:    config.Port,
        timeout: config.Timeout,
        debug:   config.Debug,
        logger:  config.Logger,
    }
}

// 사용
server := NewServer(ServerConfig{
    Port:  9090,
    Debug: true,
})

4.3 함수형 옵션 패턴 (Functional Options) 🌟

고도로 유연한 설정이 필요한 라이브러리나 프레임워크에서 널리 사용된다. gRPC, etcd, Kubernetes 등에서 활용됨.

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 WithLogger(l *log.Logger) Option {
    return func(s *Server) {
        s.logger = l
    }
}

func NewServer(options ...Option) *Server {
    server := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        logger:  log.Default(),
    }

    for _, opt := range options {
        opt(server)
    }

    return server
}

// 사용
server := NewServer(
    WithHost("api.example.com"),
    WithPort(8000),
    WithTimeout(10*time.Second),
    WithLogger(customLogger),
)

이 방식의 장점은:

  • 옵션을 자유롭게 조합 가능
  • 새 옵션이 추가되어도 기존 코드 영향 없음
  • 코드의 의도가 매우 명확함
  • JSON/YAML 등으로 직렬화 가능한 구성 정보 제공

5. 결론: '빈곤함'이 오히려 '풍요'다

Go가 기본값 파라미터를 거부한 것은 기술적 비효율이 아니라, 장기적인 유지보수성과 코드 가독성을 위한 선택이다. 명시적인 구성은 더 많은 코드를 요구하지만, 그만큼 의도가 분명하고, 오류가 발생할 여지를 줄인다.

실제로, 함수형 옵션 패턴은 기능적으로 기본값보다 훨씬 더 유연하며, 확장성과 테스트 용이성에서도 앞선다.

결국, '더 적은 기능'을 선택한 것이 아니라, '더 나은 설계'를 선택한 것이다.

태그: go functional options configuration struct explicit design code clarity

7월 7일 21:54에 게시됨