AmberAx

[Go] Options 패턴과 Builder 패턴

· 3 min read
[Go] Options 패턴과 Builder 패턴

Go 언어에서 특정 패키지나 구조체를 초기화할 때, New 함수가 흔히 사용됩니다. 하지만 프로젝트가 발전하면서 New 함수의 인자가 점점 많아지거나, 선택적인 인자가 추가되는 경우, 코드가 복잡해지고 사용성이 떨어질 수 있습니다.

예를 들어, 초기에는 2~3개의 인자로 충분했던 함수가 시간이 지나면서 10개 이상의 인자를 요구하거나, 필수가 아닌 값을 처리해야 하는 상황이 생길 수 있습니다.

이 글에서는 Options 패턴Builder 패턴이라는 두 가지 디자인 패턴을 활용해 이런 문제를 해결하는 방법을 소개합니다.


문제 상황: 포트 설정 예시 #

서버 초기화 시 포트를 설정한다고 가정해봅시다. 다음과 같은 요구 사항이 있습니다:

  1. 포트를 지정하지 않으면 기본 포트를 사용합니다.
  2. 포트가 0이면 랜덤 포트를 사용합니다.
  3. 포트가 지정되면 해당 포트를 사용합니다.

이 경우, 포트가 “설정되지 않음"을 구분하기 위해 포인터 타입(*int)을 사용해야 할 수도 있습니다. 이런 코드는 다음과 같이 번거로울 수 있습니다:

port := 1234
NewServer(&port)

이런 문제를 해결하기 위해 Options 패턴Builder 패턴을 활용하면 코드의 가독성과 유지 보수성을 크게 향상시킬 수 있습니다.


Options 패턴 #

개념

Options 패턴은 구조체나 함수를 초기화할 때, 선택적인 설정값을 유연하게 전달할 수 있도록 설계된 패턴입니다. 보통 가변 인자와 함수형 옵션을 결합해 구현됩니다.

장점

  • 기본값 관리가 쉬워집니다.
  • 필요한 옵션만 설정할 수 있어 호출 코드가 간결해집니다.
  • 명시적이고 확장성 있는 설계가 가능합니다.

구현 예시

다음은 Options 패턴을 사용하는 Config 구조체의 예입니다.

package main

import "fmt"

// Config 구조체 정의
type Config struct {
    Host string
    Port int
}

// Option 타입 정의
type Option func(*Config)

// 옵션 함수 정의
func WithHost(host string) Option {
    return func(c *Config) {
        c.Host = host
    }
}

func WithPort(port int) Option {
    return func(c *Config) {
        c.Port = port
    }
}

// NewConfig 함수
func NewConfig(opts ...Option) *Config {
    // 기본값 초기화
    config := &Config{
        Host: "localhost",
        Port: 8080,
    }
    // 옵션 적용
    for _, opt := range opts {
        opt(config)
    }
    return config
}

func main() {
    // 기본값 사용
    config1 := NewConfig()
    fmt.Printf("Config1: %+v
", config1)

    // 옵션 설정
    config2 := NewConfig(WithHost("example.com"), WithPort(9090))
    fmt.Printf("Config2: %+v
", config2)
}

실행 결과

Config1: {Host:localhost Port:8080}
Config2: {Host:example.com Port:9090}

Options 패턴의 확장성

옵션 함수는 추가적인 설정값이 필요할 때 손쉽게 확장할 수 있습니다. 예를 들어, 보안 설정을 추가하려면 다음과 같은 옵션 함수를 정의할 수 있습니다:

func WithTLS(enableTLS bool) Option {
    return func(c *Config) {
        c.TLS = enableTLS
    }
}

Builder 패턴 #

개념

Builder 패턴은 복잡한 객체를 단계적으로 설정하고 생성할 수 있도록 설계된 패턴입니다. 체이닝 방식을 활용해 읽기 쉬운 코드를 작성할 수 있습니다.

장점

  • 체이닝을 통해 가독성이 높아집니다.
  • 설정 과정이 명확하고 직관적입니다.
  • 복잡한 객체 초기화에 적합합니다.

구현 예시

다음은 Builder 패턴을 사용하는 Config 구조체의 예입니다.

package main

import "fmt"

// Config 구조체 정의
type Config struct {
    Host string
    Port int
}

// Builder 정의
type ConfigBuilder struct {
    config *Config
}

// NewConfigBuilder 생성자
func NewConfigBuilder() *ConfigBuilder {
    return &ConfigBuilder{config: &Config{
        Host: "localhost", // 기본값
        Port: 8080,        // 기본값
    }}
}

// 옵션 설정 메서드
func (b *ConfigBuilder) SetHost(host string) *ConfigBuilder {
    b.config.Host = host
    return b
}

func (b *ConfigBuilder) SetPort(port int) *ConfigBuilder {
    b.config.Port = port
    return b
}

// 빌드 메서드
func (b *ConfigBuilder) Build() *Config {
    return b.config
}

func main() {
    // 기본값 사용
    config1 := NewConfigBuilder().Build()
    fmt.Printf("Config1: %+v
", config1)

    // 옵션 설정
    config2 := NewConfigBuilder().
        SetHost("example.com").
        SetPort(9090).
        Build()
    fmt.Printf("Config2: %+v
", config2)
}

실행 결과

Config1: {Host:localhost Port:8080}
Config2: {Host:example.com Port:9090}

Builder 패턴의 한계

Builder 패턴은 체이닝 방식으로 구현되기 때문에 중간 단계에서 에러를 반환하거나 처리하기 어렵습니다. 따라서 유효성 검증은 Build 메서드에서 수행해야 하며, 이로 인해 코드가 복잡해질 수 있습니다.


Options 패턴과 Builder 패턴 비교 #

특징 Options 패턴 Builder 패턴
주요 목적 동적이고 간단한 옵션 설정 복잡한 객체 초기화
가독성 명시적이나 체이닝은 불가 체이닝으로 읽기 쉬움
확장성 간단히 함수 추가 가능 복잡한 설정 과정을 캡슐화 가능
사용 사례 간단한 초기화가 필요한 경우 복잡한 초기화 로직이 필요한 경우

결론 #

Options 패턴과 Builder 패턴은 Go에서 초기화 코드를 개선할 수 있는 강력한 도구입니다.

  1. Options 패턴: 간단한 초기화와 기본값 관리에 적합합니다. 설정값이 많아질 때도 호출자 입장에서 사용이 간편합니다.
  2. Builder 패턴: 단계적으로 복잡한 설정이 필요한 경우 유용합니다. 체이닝 방식 덕분에 가독성이 높은 코드를 작성할 수 있습니다.

프로젝트의 요구사항에 따라 두 패턴을 적절히 활용하여 코드를 더 깔끔하고 유연하게 만들어 보세요.

Did you find this post helpful?
Share it with others!