Menu
Coddy logo textTech

함수형 옵션 패턴

Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 79번째.

많은 선택적 설정 필드를 가진 구조체를 생성할 때, 생성자 함수는 다루기 힘들어질 수 있습니다. 함수형 옵션 패턴(Functional Options Pattern)은 함수를 사용하여 객체를 설정함으로써 이 문제를 우아하게 해결합니다.

여러 개의 선택적 설정을 가진 서버 구조체(struct)를 고려해 봅시다. 매개변수가 많은 생성자 대신, 옵션 타입과 옵션을 반환하는 함수들을 정의합니다:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

생성자는 가변 인자 옵션 목록을 받아 각각을 적용합니다:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // 기본값
        timeout: 30,      // 기본값
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

이제 서버 생성이 가독성 있고 유연해집니다:

// 기본값 사용
s1 := NewServer("localhost")

// 특정 옵션 사용자 정의
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

이 패턴은 호출자가 필요한 것만 지정하면 되고, 기본값이 한 곳에 명확하게 정의되어 있으며, 새로운 옵션을 추가해도 기존 코드가 깨지지 않는다는 점에서 매우 유용합니다. grpczap과 같은 Go 라이브러리에서 널리 사용됩니다.

challenge icon

챌린지

쉬움

함수형 옵션 패턴(Functional Options Pattern)을 사용하여 설정 가능한 데이터베이스 연결을 만들어 봅시다! 이 패턴은 합리적인 기본값을 가진 많은 선택적 설정이 있을 때 빛을 발하며, 이는 데이터베이스 연결에 정확히 필요한 기능입니다.

코드는 두 개의 파일로 구성합니다:

  • database.go: 데이터베이스 연결 타입과 옵션 함수들을 정의합니다.

    다음과 같은 비공개(unexported) 필드를 가진 DBConnection 구조체를 생성하세요:

    • host (string)
    • port (int)
    • username (string)
    • password (string)
    • maxConnections (int)
    • timeout (int) - 초 단위

    *DBConnection을 수정하는 함수 타입인 Option 타입을 정의하세요.

    Option을 반환하는 다음 옵션 함수들을 생성하세요:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    *DBConnection을 반환하는 생성자 NewDBConnection(host string, opts ...Option)를 생성하세요. 기본값은 다음과 같이 설정합니다: port 5432, username "admin", password "secret", maxConnections 10, timeout 30. 기본값을 설정한 후 제공된 모든 옵션을 적용하세요.

    다음 형식으로 연결 정보를 반환하는 ConnectionString() 메서드를 추가하세요:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: 다양한 설정을 사용하여 데이터베이스 연결을 구축합니다.

    호스트를 읽은 다음, 적용할 옵션의 개수를 읽습니다. 각 옵션에 대해 옵션 타입과 해당 값(들)을 읽습니다:

    • port 다음에 포트 번호
    • credentials 다음에 사용자 이름과 비밀번호 (두 줄)
    • maxconn 다음에 최대 연결 수
    • timeout 다음에 초 단위 타임아웃

    지정된 옵션으로 DBConnection을 생성하고 해당 연결 문자열을 출력하세요.

다음 입력이 제공됩니다:

  • 1행: 호스트 이름
  • 2행: 옵션 개수
  • 다음 행들: 옵션 타입 및 값(들)

예를 들어, 다음과 같이 입력되면:

localhost
0

출력은 다음과 같아야 합니다:

admin:secret@localhost:5432 (max:10, timeout:30s)

그리고 다음과 같이 입력되면:

db.example.com
2
port
3306
credentials
root
mypassword

출력은 다음과 같아야 합니다:

root:mypassword@db.example.com:3306 (max:10, timeout:30s)

그리고 다음과 같이 입력되면:

production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60

출력은 다음과 같아야 합니다:

dbuser:securepass123@production.server:5433 (max:50, timeout:60s)

호출자가 필요한 옵션만 지정하고, 지정되지 않은 설정은 합리적인 기본값을 사용하는 방식에 주목하세요. 이것이 바로 함수형 옵션 패턴의 우아함입니다!

치트 시트

함수형 옵션 패턴(Functional Options Pattern)은 긴 생성자 매개변수 대신 함수를 사용하여 많은 선택적 필드를 가진 구조체를 구성하는 우아한 방법을 제공합니다.

구조체에 대한 포인터를 수정하는 함수로 Option 타입을 정의합니다:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

Option 클로저를 반환하는 옵션 함수들을 생성합니다:

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

생성자는 가변 인자 옵션 리스트를 받아 기본값을 설정한 다음, 각 옵션을 적용합니다:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // default
        timeout: 30,      // default
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

사용법은 깔끔하고 유연합니다. 호출자는 필요한 것만 지정하면 됩니다:

// 기본값 사용
s1 := NewServer("localhost")

// 특정 옵션 사용자 정의
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

장점: 기본값이 중앙 집중화되고, 호출자는 필요한 것만 지정하며, 새로운 옵션을 추가해도 기존 코드가 깨지지 않습니다.

직접 해보기

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// 호스트 읽기
	host, _ := reader.ReadString('\n')
	host = strings.TrimSpace(host)

	// 옵션 개수 읽기
	numOptionsStr, _ := reader.ReadString('\n')
	numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))

	// 옵션 수집
	var opts []Option

	for i := 0; i < numOptions; i++ {
		optionType, _ := reader.ReadString('\n')
		optionType = strings.TrimSpace(optionType)

		switch optionType {
		case "port":
			portStr, _ := reader.ReadString('\n')
			port, _ := strconv.Atoi(strings.TrimSpace(portStr))
			// TODO: opts에 WithPort 옵션 추가
			_ = port // 구현 시 이 라인을 제거하세요

		case "credentials":
			username, _ := reader.ReadString('\n')
			username = strings.TrimSpace(username)
			password, _ := reader.ReadString('\n')
			password = strings.TrimSpace(password)
			// TODO: opts에 WithCredentials 옵션 추가
			_, _ = username, password // 구현 시 이 라인을 제거하세요

		case "maxconn":
			maxStr, _ := reader.ReadString('\n')
			maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
			// TODO: opts에 WithMaxConnections 옵션 추가
			_ = maxConn // 구현 시 이 라인을 제거하세요

		case "timeout":
			timeoutStr, _ := reader.ReadString('\n')
			timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
			// TODO: opts에 WithTimeout 옵션 추가
			_ = timeout // 구현 시 이 라인을 제거하세요
		}
	}

	// TODO: host와 opts를 사용하여 NewDBConnection으로 DBConnection 생성
	// TODO: ConnectionString() 메서드를 사용하여 연결 문자열 출력

	_ = opts // 구현 시 이 라인을 제거하세요
	fmt.Println("TODO: Print connection string here")
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨