Builder 1. Builder and Nested Builder

Builder 1. Builder and Nested Builder

코드를 작성하다 보면 생성자(constructor)가 너무 복잡해지는 상황에 자주 직면하게 됩니다. 특히 객체의 속성이 많아지거나 다양한 방식으로 객체를 생성해야 할 때, 이 문제는 더욱 두드러집니다. 이때 Builder 패턴이 매우 유용하게 쓰일 수 있습니다. Builder 패턴은 객체를 단계별로 생성하고, 객체 생성 절차를 명확히 하여 가독성, 유지보수성, 그리고 확장성을 높이는 데 도움을 줍니다.

이번 글에서는 Builder 패턴이 어떻게 코드의 복잡성을 줄이고, 객체 생성 절차를 더 체계적으로 관리하는지 알아보겠습니다.


1. Code Smell

1-1. Constructor Hell

하나의 클래스가 너무 많은 생성자와 다양한 매개변수를 가질 때 발생하는 문제를 Constructor Hell이라 부릅니다. 이 경우, 생성자의 수가 늘어나면서 코드가 복잡해지고 유지보수가 어려워지며, 실수로 잘못된 생성자를 호출할 가능성도 높아집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Enemy
{
    public string Name;
    public int Health;
    public float Speed;
    public bool IsBoss;

    public Enemy(string name)
    {
        this.Name = name;
    }

    public Enemy(string name, int health) : this(name)
    {
        this.Health = health;
    }

    public Enemy(string name, int health, float speed) : this(name, health)
    {
        this.Speed = speed;
    }

    public Enemy(string name, int health, float speed, bool isBoss) : this(name, health, speed)
    {
        this.IsBoss = isBoss;
    }
}

생성자가 점점 길어지고 복잡해지는 “Telescoping Constructor” 문제를 직면할 수 있습니다.

1-2. Complex Object Creation

복잡한 객체를 생성할 때 여러 단계나 다양한 속성을 설정해야 할 경우, 객체의 초기화 과정이 매우 복잡해집니다. 이런 상황에서는 복잡한 로직을 간단하고 체계적으로 처리할 수 있는 방법이 필요합니다.

1
2
3
4
5
6
var enemies = new List<Enemy>()
{
    new Enemy(){ Name = "Goblin", Health = 100},
    new Enemy(){ Name = "Orc", Health = 80, Speed = 30},
    new Enemy(){ Name = "Dragon", Health = 150, Speed = 50, IsBoss = true},
};

이처럼 복잡한 객체 생성을 더 간결하게 처리할 방법이 필요할 때 Builder 패턴을 도입할 수 있습니다.


2. Builder

Builder 패턴은 객체의 생성 과정을 분리하여 단계적으로 설정할 수 있게 해줍니다. 이를 통해 생성자에 너무 많은 매개변수를 전달하지 않고도 객체를 유연하게 설정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Enemy
{
    public string Name { get; set;}
    public int Health {get; set;}
    public float Speed { get; set;}
    public bool IsBoss { get; set;}
}

public class Builder
{
    private Enemy enemy = new Enemy();

    public void SetName(string name)
    {
        enemy.Name = name;
    }

    public void SetHealth(int health)
    {
        enemy.Health = health;
    }

    public void SetSpeed(float speed)
    {
        enemy.Speed = speed;
    }

    public void SetIsBoss(bool isBoss)
    {
        enemy.IsBoss = isBoss;
    }

    public Enemy Build()
    {
        return enemy;
    }
}

이렇게 객체를 빌더 클래스를 통해 생성하고, 필요할 때마다 Set 메서드를 호출하여 속성을 설정할 수 있습니다.


2-1. Director : Builder 의 Facade

**Director **는 객체 생성 과정을 관리하는 역할을 합니다. 빌더 패턴과 조합하여, 특정 객체를 일관된 방식으로 쉽게 생성할 수 있도록 돕습니다. 이는 다양한 객체 구성을 필요로 할 때 특히 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Director
{
    private Builder builder;

    public Director(Builder builder)
    {
        this.builder = builder;
    }

    public Enemy CreateGoblin()
    {
        builder.SetName("Goblin");
        builder.SetHealth(100);
        builder.SetSpeed(2.5f);
        builder.SetIsBoss(false);

        return builder.Build();
    }
}

Director는 특정 객체를 어떻게 설정할지 미리 정의하고, 그 설정을 일관되게 유지할 수 있게 해줍니다.


3. 장점과 단점

3-1. 장점

  • 읽기 쉬움: 객체 생성 과정이 명확하게 단계별로 나누어져 있어 코드가 더 직관적이고 읽기 쉽습니다.
  • 유지보수성 향상: 생성자에 많은 매개변수를 전달하는 대신, 빌더 패턴을 통해 객체 설정이 유연해지므로 유지보수가 쉬워집니다.
  • 유연성: 객체를 빌드하는 각 단계가 분리되어 있어 다양한 방식으로 객체를 쉽게 생성할 수 있습니다.

3-2. 단점

  • 간단한 객체에 불필요한 복잡성 추가: 객체가 간단할 경우, 빌더 패턴은 오히려 불필요한 복잡성을 추가할 수 있습니다. 객체가 복잡하지 않다면, 이 패턴이 오버엔지니어링일 수 있습니다.

4. Nested Builder : 다른 생성 방식을 방지하자

Nested Builder는 생성자에서 객체의 직접적인 생성을 막고, 빌더 클래스를 통해서만 객체를 생성할 수 있게 하는 방식입니다. 이를 통해 객체의 상태를 더 안전하게 초기화하고 관리할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Enemy
{
    public string Name { get; private set; }
    public int Health { get; private set; }
    public float Speed { get; private set; }
    public bool IsBoss { get; private set; }

    private Enemy() {}

    public class Builder
    {
        private Enemy enemy = new Enemy();

        public void SetName(string name)
        {
            enemy.Name = name;
        }

        public void SetHealth(int health)
        {
            enemy.Health = health;
        }

        public void SetSpeed(float speed)
        {
            enemy.Speed = speed;
        }

        public void SetIsBoss(bool isBoss)
        {
            enemy.IsBoss = isBoss;
        }

        public Enemy Build()
        {
            return enemy;
        }
    }
}

이 방식은 객체의 직접적인 생성을 막고, 빌더를 통해서만 객체를 생성하게 합니다. 이를 통해 객체 생성 로직을 더 안전하게 관리할 수 있으며, 캡슐화와 보안성이 강화됩니다.


마무리

Builder 패턴은 복잡한 객체 생성을 보다 체계적이고 안전하게 처리할 수 있는 훌륭한 도구입니다. 특히 여러 매개변수를 가진 객체를 유연하게 생성할 때 매우 유용합니다. 이를 통해 코드를 더 읽기 쉽게 만들고, 유지보수를 쉽게 할 수 있으며, 객체 생성 과정의 오류 가능성을 줄일 수 있습니다.

SWeetJelly
SWeetJelly Programmer, Game Designer
comments powered by Disqus