[내일배움캠프 TIL] 디자인 패턴 - 빌더 패턴

KYJ의 Tech Velog·2023년 10월 12일
0

TIL

목록 보기
45/71
post-thumbnail

Today I Learned

오늘은 디자인 패턴, 그 중에서도 빌더 패턴에 대해 이야기해보도록 하겠습니다.

Today I Done

빌더 패턴

복잡한 객체의 생성 과정과 표현 방법을 분리하여 다양한 구성의 인스턴스를 만드는 생성 패턴입니다.

생성 패턴은 인스턴스를 만드는 절차를 추상화하는 패턴들을 의미합니다. (ex. 싱글톤 패턴, 팩토리 패턴, 추상 팩토리 패턴 등...)

이러한 생성 패턴은 시스템이 상속(Inheritance)보다 복합(Composite)을 사용하는 방향으로 진화/지향되어 가면서 더 중요해지고 있습니다.

우리가 어떤 클래스를 생성해야 할 때, 다양한 인자들을 여러 경우의 수로 받아야 하는 경우가 있다고 가정합시다. 인자의 타입, 순서 등에 대한 관리가 어려워져 오류가 발생할 확률이 높아집니다. 또한, 필요없는 인자들에 대한 처리도 일일이 해주어야 합니다.

빌더 패턴은 복잡한 생성 과정을 명시적이고 단계적으로 만들어 위의 문제를 해결해줍니다. 별도의 빌더 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값들에 대해서는 메서드를 통해 값을 입력받습니다. 최종적으로 빌드 메서드를 호출하여 하나의 인스턴스를 반환합니다.

예시로 자동차 클래스를 만들어보겠습니다. 자동차는 여러 가지 필수 요소(핸들, 바퀴, 엔진 등)와, 여러 가지 선택 옵션(블랙박스, 네비게이션, 썬루프 등)들로 구성되어 있습니다.

using System;

class Car
{
    // 필수 요소
    private string steeringWheel;
    private int tires;
    private string engine;

    // 선택적 요소
    private bool sunroof;
    private bool navigation;
    private bool blackbox;

    public Car(string steeringWheel, int tires, string engine)
    {
        this.steeringWheel = steeringWheel;
        this.tires = tires;
        this.engine = engine;
    }

    public Car(string steeringWheel, int tires, string engine, bool sunroof)
        : this(steeringWheel, tires, engine)
    {
        this.sunroof = sunroof;
    }

    public Car(string steeringWheel, int tires, string engine, bool sunroof, bool navigation)
        : this(steeringWheel, tires, engine, sunroof)
    {
        this.navigation = navigation;
    }

    public Car(string steeringWheel, int tires, string engine, bool sunroof, bool navigation, bool blackbox)
        : this(steeringWheel, tires, engine, sunroof, navigation)
    {
        this.blackbox = blackbox;
    }

    public override string ToString()
    {
        return $"Steering Wheel: {steeringWheel}, Tires: {tires}, Engine: {engine}, Sunroof: {sunroof}, Navigation: {navigation}, Blackbox: {blackbox}";
    }
}

class Program
{
    static void Main()
    {
        // 생성자를 사용하여 필수 매개변수를 전달하고, 선택 매개변수를 설정하여 자동차 객체를 생성
        Car car1 = new Car("Leather", 4, "V6", true, true, true);
        Car car2 = new Car("Standard", 4, "Inline-4", true, false, false);

        Console.WriteLine("Car 1 Details:");
        Console.WriteLine(car1);

        Console.WriteLine("\nCar 2 Details:");
        Console.WriteLine(car2);
    }
}

이런 방식으로 구현하게 되면 인스턴스 필드가 많아지면 많아질수록 생성자에 들어갈 인자의 수가 들어나 몇 번째 인자가 어떤 값이었는지 헷갈리는 경우가 생깁니다. 다양한 자동차를 생성하기 위해 생정자의 인자의 순서를 정확히 알고 있어야 하죠.

또한 인자의 특성 상 순서를 지켜야 하기 때문에 ‘썬루프와 블랙박스를 옵션으로 가지는 차’를 생성하고 싶을 경우 네비게이션 자리에는 false 값을 넣어주어야 합니다. 생성자만으로는 필드를 선택적으로 생략할 수 있는 방법이 위에서는 존재하지 않기 때문입니다.

무엇보다 인자의 종류가 다양할 수록 생성자의 종류도 늘어나기 때문에 가독성이나 유지 보수 측면에서 굉장히 좋지 않습니다.


이를 해결하기 위한 가장 간단한 방법을 Setter 메서드를 사용하는 것입니다.

using System;

class Car
{
    // 필수 매개변수
    private string steeringWheel;
    private int tires;
    private string engine;

    // 선택 매개변수 (기본값으로 초기화)
    private bool sunroof = false;
    private bool navigation = false;
    private bool blackbox = false;

    public Car(string steeringWheel, int tires, string engine)
    {
        this.steeringWheel = steeringWheel;
        this.tires = tires;
        this.engine = engine;
    }

    public void SetSunroof(bool sunroof)
    {
        this.sunroof = sunroof;
    }

    public void SetNavigation(bool navigation)
    {
        this.navigation = navigation;
    }

    public void SetBlackbox(bool blackbox)
    {
        this.blackbox = blackbox;
    }

    public override string ToString()
    {
        return $"Steering Wheel: {steeringWheel}, Tires: {tires}, Engine: {engine}, Sunroof: {sunroof}, Navigation: {navigation}, Blackbox: {blackbox}";
    }
}

class Program
{
    static void Main()
    {
        // Setter 메서드를 사용하여 자동차 객체 생성 및 설정
        Car car1 = new Car("Leather", 4, "V6");
        car1.SetSunroof(true);
        car1.SetNavigation(true);
        car1.SetBlackbox(true);

        Car car2 = new Car("Standard", 4, "Inline-4");
        car2.SetSunroof(true);
        car2.SetNavigation(false);
        car2.SetBlackbox(false);

        Console.WriteLine("Car 1 Details:");
        Console.WriteLine(car1);

        Console.WriteLine("\nCar 2 Details:");
        Console.WriteLine(car2);
    }
}

가독성 문제점은 사라집니다. 또한, 여러 옵션들에 해당하는 필드의 경우 Setter 메서드를 호출해서 유연하게 객체 생성이 가능해집니다.

하지만 이 방식도 객체 생성 시점에 모든 값들을 주입하지 않으면 일관성 문제와 불변성 문제가 발생할 수 있습니다. 객체를 생성하고 Setter 메서드를 이용해서 외부에서 함부로 객체를 조작하는 것이 가능합니다.


빌더 패턴을 통해 모든 문제를 해결할 수 있습니다.

  1. Car 클래스
using System;

public class Car
{
    private string steeringWheel;
    private int tires;
    private string engine;
    private bool sunroof;
    private bool navigation;
    private bool blackbox;

    private Car(string steeringWheel, int tires, string engine)
    {
        this.steeringWheel = steeringWheel;
        this.tires = tires;
        this.engine = engine;
    }

    public override string ToString()
    {
        return $"Steering Wheel: {steeringWheel}, Tires: {tires}, Engine: {engine}, Sunroof: {sunroof}, Navigation: {navigation}, Blackbox: {blackbox}";
    }
}
  1. 빌더 클래스
public class Builder
{
    private string steeringWheel;
    private int tires;
    private string engine;

    private bool sunroof = false;
    private bool navigation = false;
    private bool blackbox = false;

    public Builder(string steeringWheel, int tires, string engine)
    {
        this.steeringWheel = steeringWheel;
        this.tires = tires;
        this.engine = engine;
    }

    public Builder WithSunroof(bool sunroof)
    {
        this.sunroof = sunroof;
        return this;
    }

    public Builder WithNavigation(bool navigation)
    {
        this.navigation = navigation;
        return this;
    }

    public Builder WithBlackbox(bool blackbox)
    {
        this.blackbox = blackbox;
        return this;
    }

    public Car Build()
    {
        return new Car(steeringWheel, tires, engine)
        {
            sunroof = this.sunroof,
            navigation = this.navigation,
            blackbox = this.blackbox
        };
    }
}
  1. 실행
class Program
{
    static void Main()
    {
        Car car1 = new Car.Builder("Leather", 4, "V6")
            .WithSunroof(true)
            .WithNavigation(true)
            .WithBlackbox(true)
            .Build();

        Car car2 = new Car.Builder("Standard", 4, "Inline-4")
            .WithSunroof(true)
            .WithNavigation(false)
            .WithBlackbox(false)
            .Build();

        Console.WriteLine("Car 1 Details:");
        Console.WriteLine(car1);

        Console.WriteLine("Car 2 Details:");
        Console.WriteLine(car2);
    }
}

프로그램 실행 시, 객체 생성은 생성자 호출과 함께 여러가지 메서드를 체이닝하여 이루어집니다. 객체 생성 시점에 모든 작업이 이루어지므로 앞서 언급한 일관성, 불변성 문제가 발생하지 않게 됩니다.

특히, 새로운 빌더를 생성하지 않고는 객체의 값을 변경할 수 없고 명시적으로 필요한 값들을 메서드로 호출하기 때문에 특정한 필드를 입력하지 못하는 오류가 줄일 수 있습니다.


매개변수가 많아질수록 객체를 생성할 때, 오류의 가능성이 많아집니다.

그러나 빌더 패턴은 직관적으로 어떤 데이터에 어떤 값이 설정되는 한눈에 파악할 수 있습니다.

다만, 최근에는 IDE에서 생성자 매개변수에 대해 미리보기 힌트 기능을 제공해주기 때문에 요즘 트렌드에는 맞지 않을 수 있습니다.

그래도 빌더 패턴의 중요한 가치는 초기화가 필수인 멤버는 빌더의 생성자로, 선택적인 멤버는 빌더의 메서드로 받도록 한다는 것입니다. 필수 멤버와 선택 멤버를 구분해서 객체 생성을 유도할 수 있고 객체를 효율적으로 관리할 수 있게 됩니다.


또한, 객체 생성을 단계적으로 구성하거나 구성 단계를 지연하거나 재귀적으로 생성을 처리할 수 있습니다.

다음과 같이 객체 생성을 지연하여 필요할 때 사용할 수 있습니다.

using System;
using System.Collections.Generic;

class Student
{
    public int StudentNumber { get; set; }
    public string Name { get; set; }
    public string Grade { get; set; }
    public string PhoneNumber { get; set; }

    public override string ToString()
    {
        return $"StudentNumber: {StudentNumber}, Name: {Name}, Grade: {Grade}, PhoneNumber: {PhoneNumber}";
    }
}

class StudentBuilder
{
    private Student student = new Student();

    public StudentBuilder(int studentNumber)
    {
        student.StudentNumber = studentNumber;
    }

    public StudentBuilder Name(string name)
    {
        student.Name = name;
        return this;
    }

    public StudentBuilder Grade(string grade)
    {
        student.Grade = grade;
        return this;
    }

    public StudentBuilder PhoneNumber(string phoneNumber)
    {
        student.PhoneNumber = phoneNumber;
        return this;
    }

    public Student Build()
    {
        return student;
    }
}

class Program
{
    static void Main()
    {
        List<StudentBuilder> builders = new List<StudentBuilder>();

        builders.Add(
            new StudentBuilder(2016120091)
            .Name("홍길동")
        );

        builders.Add(
            new StudentBuilder(2016120092)
            .Name("임꺽정")
            .Grade("senior")
        );

        builders.Add(
            new StudentBuilder(2016120093)
            .Name("박혁거세")
            .Grade("sophomore")
            .PhoneNumber("010-5555-5555")
        );

        foreach (StudentBuilder builder in builders)
        {
            Student student = builder.Build();
            Console.WriteLine(student);
        }
    }
}

빌더 패턴은 생성 과정을 하나로 만들어 일관성 불변성을 확보한다고 하였습니다. 이를 통해 우리는 동기화 문제를 덜 고려할 수 있습니다.

또한, 작업 도중 예와가 발생하여 객체가 불안정한 상태에 빠지는 것을 고려하지 않아도 됩니다.

다만, 빌더 패턴을 적용하려면 N개의 클래스에 대해 N개의 새로운 빌더 클래스를 만들어야 합니다. 따라서, 클래스의 수가 기하급수적으로 늘어나 관리해야할 클래스가 많아지고 구조가 복잡해질 수 있다는 단점이 있습니다.

Tomorrow's Goal

  • 팀프로젝트 시작
  • 코드 카타 Clear

0개의 댓글