구조체 생성자 패턴
Go에는 클래스와 생성자(constructor)가 없습니다. 대신 New 함수 패턴 과 함수형 옵션 패턴(Functional Options Pattern) 을 사용하여 안전하고 유연한 객체 생성을 구현합니다. 이 패턴들은 실제 Go 표준 라이브러리와 대부분의 Go 오픈소스 프로젝트에서 사용됩니다.
기본 구조체 리터럴
가장 단순한 방법은 구조체 리터럴로 직접 생성하는 것입니다.
package main
import "fmt"
type Point struct {
X, Y float64
}
type Color struct {
R, G, B uint8
}
type Pixel struct {
Point
Color
}
func main() {
// 필드명 없이 순서대로 초기화 (비권장 — 필드 추가 시 깨짐)
p1 := Point{3.0, 4.0}
// 필드명 명시 (권장)
p2 := Point{X: 3.0, Y: 4.0}
// 부분 초기화 (나머지는 제로값)
p3 := Point{X: 5.0} // Y는 0.0
fmt.Println(p1, p2, p3)
// 임베딩 구조체 초기화
px := Pixel{
Point: Point{X: 10, Y: 20},
Color: Color{R: 255, G: 0, B: 0},
}
fmt.Println(px)
}
New 함수 패턴
유효성 검사나 기본값 설정이 필요할 때 New 함수로 생성자를 구현합니다.
package main
import (
"errors"
"fmt"
"strings"
)
type User struct {
id int // 소문자 = 비공개 필드
name string
email string
isActive bool
}
// 생성자 함수 — 유효성 검사 포함
func NewUser(id int, name, email string) (*User, error) {
if id <= 0 {
return nil, errors.New("ID는 양수여야 합니다")
}
name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("이름은 필수입니다")
}
if !strings.Contains(email, "@") {
return nil, fmt.Errorf("유효하지 않은 이메일: %s", email)
}
return &User{
id: id,
name: name,
email: strings.ToLower(email),
isActive: true, // 기본값 설정
}, nil
}
// Getter 메서드 (비공개 필드 접근)
func (u *User) ID() int { return u.id }
func (u *User) Name() string { return u.name }
func (u *User) Email() string { return u.email }
func (u *User) IsActive() bool { return u.isActive }
func (u *User) Deactivate() {
u.isActive = false
}
func (u User) String() string {
status := "활성"
if !u.isActive {
status = "비활성"
}
return fmt.Sprintf("User{id=%d, name=%s, email=%s, status=%s}",
u.id, u.name, u.email, status)
}
func main() {
// 정상 생성
user, err := NewUser(1, "홍길동", "hong@example.com")
if err != nil {
fmt.Println("에러:", err)
return
}
fmt.Println(user)
// 유효성 검사 실패
_, err = NewUser(0, "테스트", "invalid-email")
fmt.Println("에러:", err) // 에러: ID는 양수여야 합니다
_, err = NewUser(2, "", "test@test.com")
fmt.Println("에러:", err) // 에러: 이름은 필수입니다
user.Deactivate()
fmt.Println(user)
}
함수형 옵션 패턴 (Functional Options Pattern)
매개변수가 많거나 선택적 설정이 필요할 때 가장 유연한 패턴입니다. grpc-go, zap, cobra 등 주요 Go 라이브러리에서 널리 사용됩니다.
package main
import (
"fmt"
"time"
)
type Server struct {
host string
port int
timeout time.Duration
maxConns int
readTimeout time.Duration
writeTimeout time.Duration
tlsEnabled bool
logLevel string
}
// Option 타입: Server를 수정하는 함수
type Option func(*Server)
// 기본값 설정 후 옵션 적용
func NewServer(host string, port int, opts ...Option) *Server {
// 합리적인 기본값
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second,
maxConns: 100,
readTimeout: 10 * time.Second,
writeTimeout: 10 * time.Second,
logLevel: "info",
}
// 옵션 함수들을 순서대로 적용
for _, opt := range opts {
opt(s)
}
return s
}
// 옵션 생성 함수들
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 WithReadTimeout(d time.Duration) Option {
return func(s *Server) {
s.readTimeout = d
}
}
func WithWriteTimeout(d time.Duration) Option {
return func(s *Server) {
s.writeTimeout = d
}
}
func WithTLS() Option {
return func(s *Server) {
s.tlsEnabled = true
}
}
func WithLogLevel(level string) Option {
return func(s *Server) {
s.logLevel = level
}
}
func (s *Server) Start() {
scheme := "http"
if s.tlsEnabled {
scheme = "https"
}
fmt.Printf("서버 시작: %s://%s:%d\n", scheme, s.host, s.port)
fmt.Printf(" 타임아웃: %v, 최대 연결: %d\n", s.timeout, s.maxConns)
fmt.Printf(" 읽기 타임아웃: %v, 쓰기 타임아웃: %v\n", s.readTimeout, s.writeTimeout)
fmt.Printf(" 로그 레벨: %s\n", s.logLevel)
}
func main() {
// 기본값만 사용
s1 := NewServer("localhost", 8080)
s1.Start()
fmt.Println()
// 옵션 추가
s2 := NewServer("0.0.0.0", 443,
WithTLS(),
WithTimeout(60*time.Second),
WithMaxConns(1000),
WithReadTimeout(30*time.Second),
WithWriteTimeout(30*time.Second),
WithLogLevel("debug"),
)
s2.Start()
}
옵션 패턴 — 유효성 검사 포함 버전
package main
import (
"errors"
"fmt"
)
type DatabaseConfig struct {
host string
port int
dbName string
user string
password string
maxOpen int
maxIdle int
sslMode string
}
type DBOption func(*DatabaseConfig) error
func NewDatabaseConfig(host, dbName, user string, opts ...DBOption) (*DatabaseConfig, error) {
if host == "" || dbName == "" || user == "" {
return nil, errors.New("host, dbName, user는 필수입니다")
}
cfg := &DatabaseConfig{
host: host,
port: 5432,
dbName: dbName,
user: user,
maxOpen: 25,
maxIdle: 5,
sslMode: "disable",
}
for _, opt := range opts {
if err := opt(cfg); err != nil {
return nil, err
}
}
return cfg, nil
}
func WithPort(port int) DBOption {
return func(cfg *DatabaseConfig) error {
if port < 1 || port > 65535 {
return fmt.Errorf("유효하지 않은 포트: %d", port)
}
cfg.port = port
return nil
}
}
func WithPassword(password string) DBOption {
return func(cfg *DatabaseConfig) error {
cfg.password = password
return nil
}
}
func WithPoolSize(maxOpen, maxIdle int) DBOption {
return func(cfg *DatabaseConfig) error {
if maxIdle > maxOpen {
return errors.New("maxIdle은 maxOpen을 초과할 수 없습니다")
}
cfg.maxOpen = maxOpen
cfg.maxIdle = maxIdle
return nil
}
}
func WithSSL() DBOption {
return func(cfg *DatabaseConfig) error {
cfg.sslMode = "require"
return nil
}
}
func (cfg *DatabaseConfig) DSN() string {
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
cfg.host, cfg.port, cfg.dbName, cfg.user, cfg.password, cfg.sslMode)
}
func main() {
cfg, err := NewDatabaseConfig("localhost", "mydb", "admin",
WithPassword("secret"),
WithPoolSize(50, 10),
WithSSL(),
)
if err != nil {
fmt.Println("설정 에러:", err)
return
}
fmt.Println("DSN:", cfg.DSN())
// 유효성 검사 실패
_, err = NewDatabaseConfig("localhost", "mydb", "admin",
WithPort(99999),
)
fmt.Println("에러:", err)
_, err = NewDatabaseConfig("localhost", "mydb", "admin",
WithPoolSize(5, 10), // maxIdle > maxOpen
)
fmt.Println("에러:", err)
}
Builder 패턴
연쇄 호출(method chaining)을 이용한 빌더 패턴도 Go에서 자주 사용됩니다.
package main
import (
"fmt"
"strings"
)
type EmailBuilder struct {
from string
to []string
cc []string
subject string
body string
html bool
errors []string
}
func NewEmailBuilder(from string) *EmailBuilder {
return &EmailBuilder{from: from}
}
func (b *EmailBuilder) To(addresses ...string) *EmailBuilder {
b.to = append(b.to, addresses...)
return b
}
func (b *EmailBuilder) CC(addresses ...string) *EmailBuilder {
b.cc = append(b.cc, addresses...)
return b
}
func (b *EmailBuilder) Subject(subject string) *EmailBuilder {
b.subject = subject
return b
}
func (b *EmailBuilder) Body(body string) *EmailBuilder {
b.body = body
return b
}
func (b *EmailBuilder) HTML() *EmailBuilder {
b.html = true
return b
}
type Email struct {
From string
To []string
CC []string
Subject string
Body string
IsHTML bool
}
func (b *EmailBuilder) Build() (*Email, error) {
var errs []string
if len(b.to) == 0 {
errs = append(errs, "수신자가 없습니다")
}
if b.subject == "" {
errs = append(errs, "제목이 없습니다")
}
if b.body == "" {
errs = append(errs, "본문이 없습니다")
}
if len(errs) > 0 {
return nil, fmt.Errorf("이메일 빌드 실패: %s", strings.Join(errs, "; "))
}
return &Email{
From: b.from,
To: b.to,
CC: b.cc,
Subject: b.subject,
Body: b.body,
IsHTML: b.html,
}, nil
}
func main() {
email, err := NewEmailBuilder("sender@example.com").
To("alice@example.com", "bob@example.com").
CC("manager@example.com").
Subject("안녕하세요!").
Body("<h1>Go 학습 중입니다</h1>").
HTML().
Build()
if err != nil {
fmt.Println("에러:", err)
return
}
fmt.Printf("From: %s\n", email.From)
fmt.Printf("To: %s\n", strings.Join(email.To, ", "))
fmt.Printf("CC: %s\n", strings.Join(email.CC, ", "))
fmt.Printf("Subject: %s\n", email.Subject)
fmt.Printf("HTML: %v\n", email.IsHTML)
// 유효성 검사 실패
_, err = NewEmailBuilder("sender@example.com").
Subject("제목").
Build()
fmt.Println("에러:", err)
}
싱글톤 패턴
전역 설정, 데이터베이스 연결 등에 사용하는 싱글톤 패턴입니다.
package main
import (
"fmt"
"sync"
)
type AppConfig struct {
Environment string
Debug bool
Version string
}
var (
instance *AppConfig
once sync.Once
)
func GetConfig() *AppConfig {
once.Do(func() {
// 실제로는 파일/환경변수에서 읽음
instance = &AppConfig{
Environment: "development",
Debug: true,
Version: "1.0.0",
}
fmt.Println("설정 초기화 완료")
})
return instance
}
func main() {
// 여러 번 호출해도 초기화는 한 번만
cfg1 := GetConfig() // "설정 초기화 완료" 출력
cfg2 := GetConfig() // 출력 없음
cfg3 := GetConfig() // 출력 없음
fmt.Println(cfg1 == cfg2) // true (같은 포인터)
fmt.Println(cfg2 == cfg3) // true
fmt.Printf("환경: %s, 버전: %s\n", cfg1.Environment, cfg1.Version)
}
패턴 선택 가이드
상황 권장 패턴 필드가 적고 모두 필수 구조체 리터럴 또는 New 함수 유효성 검사 필요 New 함수 (error 반환) 선택적 옵션이 많음 함수형 옵션 패턴 단계별 복잡한 빌드 Builder 패턴 전역 단일 인스턴스 싱글톤 (sync.Once)