본문으로 건너뛰기

구조체 생성자 패턴

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)