[Go] Options 패턴과 Builder 패턴
![[Go] Options 패턴과 Builder 패턴](https://amberax.com/go-options-builder/cover_hu3455178128500534460.png)
Go 언어에서 특정 패키지나 구조체를 초기화할 때, New
함수가 흔히 사용됩니다. 하지만 프로젝트가 발전하면서 New
함수의 인자가 점점 많아지거나, 선택적인 인자가 추가되는 경우, 코드가 복잡해지고 사용성이 떨어질 수 있습니다.
예를 들어, 초기에는 2~3개의 인자로 충분했던 함수가 시간이 지나면서 10개 이상의 인자를 요구하거나, 필수가 아닌 값을 처리해야 하는 상황이 생길 수 있습니다.
이 글에서는 Options 패턴과 Builder 패턴이라는 두 가지 디자인 패턴을 활용해 이런 문제를 해결하는 방법을 소개합니다.
문제 상황: 포트 설정 예시 #
서버 초기화 시 포트를 설정한다고 가정해봅시다. 다음과 같은 요구 사항이 있습니다:
- 포트를 지정하지 않으면 기본 포트를 사용합니다.
- 포트가
0
이면 랜덤 포트를 사용합니다. - 포트가 지정되면 해당 포트를 사용합니다.
이 경우, 포트가 “설정되지 않음"을 구분하기 위해 포인터 타입(*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에서 초기화 코드를 개선할 수 있는 강력한 도구입니다.
- Options 패턴: 간단한 초기화와 기본값 관리에 적합합니다. 설정값이 많아질 때도 호출자 입장에서 사용이 간편합니다.
- Builder 패턴: 단계적으로 복잡한 설정이 필요한 경우 유용합니다. 체이닝 방식 덕분에 가독성이 높은 코드를 작성할 수 있습니다.
프로젝트의 요구사항에 따라 두 패턴을 적절히 활용하여 코드를 더 깔끔하고 유연하게 만들어 보세요.