디자인패턴: Builder 패턴
Builder 패턴의 특징과 각종 구현 방식에 대해 알아보자.
디자인패턴: Builder 패턴
빌더 패턴 완벽 가이드
📌 빌더 패턴이란?
빌더 패턴은 복잡한 객체를 단계별로 생성하기 위한 생성 디자인 패턴입니다. 특히 많은 매개변수를 가진 객체나 선택적 매개변수가 많은 객체를 생성할 때 유용합니다.
왜 빌더 패턴이 필요한가?
복잡한 객체 생성 시 발생하는 문제들:
- 텔레스코핑 생성자 문제: 매개변수가 늘어날수록 생성자가 복잡해짐
- 가독성 저하: 어떤 매개변수가 무엇을 의미하는지 파악 어려움
- 실수 위험: 매개변수 순서를 착각하기 쉬움
- 유효성 검증 분산: 객체 생성 과정에서 일관된 검증이 어려움
🎯 기본 빌더 패턴 구현
HTML 요소 생성 예제
가장 기본적인 Fluent Builder 패턴을 HTML 요소 생성을 통해 알아보겠습니다.
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// HTML 요소 클래스
public class HtmlElement
{
public string Name, Text;
public List<HtmlElement> Elements = new List<HtmlElement>();
private const int indentSize = 2;
public HtmlElement(string name, string text)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Text = text ?? string.Empty;
}
public override string ToString()
{
return ToStringImpl(0);
}
private string ToStringImpl(int indent)
{
var sb = new StringBuilder();
var i = new string(' ', indentSize * indent);
sb.AppendLine($"{i}<{Name}>");
if (!string.IsNullOrWhiteSpace(Text))
{
sb.Append(new string(' ', indentSize * (indent + 1)));
sb.AppendLine(Text);
}
foreach (var e in Elements)
{
sb.Append(e.ToStringImpl(indent + 1));
}
sb.AppendLine($"{i}</{Name}>");
return sb.ToString();
}
}
// HTML 빌더 클래스
public class HtmlBuilder
{
private readonly string rootName;
HtmlElement root = new HtmlElement();
public HtmlBuilder(string rootName)
{
this.rootName = rootName;
root.Name = rootName;
}
public HtmlBuilder AddChild(string childName, string childText)
{
var e = new HtmlElement(childName, childText);
root.Elements.Add(e);
return this; // 메서드 체이닝을 위한 자기 자신 반환
}
public override string ToString() => root.ToString();
public void Clear()
{
root = new HtmlElement { Name = rootName };
}
}
사용 예제:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var html = new HtmlBuilder("ul")
.AddChild("li", "hello")
.AddChild("li", "world");
Console.WriteLine(html.ToString());
// 출력:
// <ul>
// <li>
// hello
// </li>
// <li>
// world
// </li>
// </ul>
🚀 고급 빌더 패턴 변형
1. Step Builder - 순서가 중요할 때
Step 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 단계별 인터페이스 정의
public interface ISpecifyCarType
{
ISpecifyWheelSize OfType(CarType type);
}
public interface ISpecifyWheelSize
{
IBuildCar WithWheels(int size);
}
public interface IBuildCar
{
Car Build();
}
// Step Builder 구현
public class CarBuilder
{
private class Impl : ISpecifyCarType, ISpecifyWheelSize, IBuildCar
{
private Car car = new Car();
public ISpecifyWheelSize OfType(CarType type)
{
car.Type = type;
return this;
}
public IBuildCar WithWheels(int size)
{
// 타입별 휠 크기 검증
switch (car.Type)
{
case CarType.Crossover when size < 17 || size > 20:
case CarType.Sedan when size < 15 || size > 17:
throw new ArgumentException($"Wrong size of wheel for {car.Type}.");
}
car.WheelSize = size;
return this;
}
public Car Build() => car;
}
public static ISpecifyCarType Create() => new Impl();
}
// 사용 예제
var car = CarBuilder.Create()
.OfType(CarType.Sedan) // 1단계: 필수
.WithWheels(16) // 2단계: 필수
.Build(); // 3단계: 완료
// 컴파일 에러 - 순서를 지키지 않으면 컴파일되지 않음
// CarBuilder.Create().WithWheels(16); // 에러!
2. Faceted Builder - 복잡한 객체의 영역별 구성
Faceted 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class Person
{
// 주소 정보
public string StreetAddress, Postcode, City;
// 직업 정보
public string CompanyName, Position;
public int AnnualIncome;
}
// 메인 Facade Builder
public class PersonBuilder
{
protected Person person = new Person();
public PersonJobBuilder Works => new PersonJobBuilder(person);
public PersonAddressBuilder Lives => new PersonAddressBuilder(person);
public static implicit operator Person(PersonBuilder pb) => pb.person;
}
// 주소 빌더
public class PersonAddressBuilder : PersonBuilder
{
public PersonAddressBuilder(Person person)
{
this.person = person;
}
public PersonAddressBuilder At(string streetAddress)
{
person.StreetAddress = streetAddress;
return this;
}
public PersonAddressBuilder In(string city)
{
person.City = city;
return this;
}
}
// 직업 빌더
public class PersonJobBuilder : PersonBuilder
{
public PersonJobBuilder(Person person)
{
this.person = person;
}
public PersonJobBuilder At(string companyName)
{
person.CompanyName = companyName;
return this;
}
public PersonJobBuilder AsA(string position)
{
person.Position = position;
return this;
}
}
// 사용 예제
Person person = new PersonBuilder()
.Lives // 주소 영역으로 전환
.At("123 London Road")
.In("London")
.Works // 직업 영역으로 전환
.At("Fabrikam")
.AsA("Engineer");
3. Functional 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
public abstract class FunctionalBuilder<TSubject, TSelf>
where TSelf : FunctionalBuilder<TSubject, TSelf>
where TSubject : new()
{
private readonly List<Func<TSubject, TSubject>> actions = new();
public TSelf Do(Action<TSubject> action) => AddAction(action);
public TSubject Build() => actions.Aggregate(new TSubject(), (p, f) => f(p));
private TSelf AddAction(Action<TSubject> action)
{
actions.Add(p => { action(p); return p; });
return (TSelf)this;
}
}
// Person Builder 구현
public sealed class PersonBuilder : FunctionalBuilder<Person, PersonBuilder>
{
public PersonBuilder Called(string name) => Do(p => p.Name = name);
}
// 확장 메서드로 기능 추가
public static class PersonBuilderExtensions
{
public static PersonBuilder WorksAs(this PersonBuilder builder, string position)
=> builder.Do(p => p.Position = position);
}
// 사용 예제
var person = new PersonBuilder()
.Called("Sarah")
.WorksAs("Developer")
.Do(p => p.Name = p.Name.ToUpper()) // 임의의 변환 가능
.Build();
📊 빌더 패턴 변형 비교
패턴 | 장점 | 단점 | 사용 시점 |
---|---|---|---|
Fluent Builder | • 간단한 구현 • 직관적 API | • 순서 강제 없음 • 타입 안전성 부족 | 일반적인 객체 생성 |
Step Builder | • 컴파일 타임 검증 • 순서 강제 | • 복잡한 구현 • 유연성 부족 | 필수 단계가 명확한 경우 |
Faceted Builder | • 책임 분리 • 확장성 우수 | • 구현 복잡도 증가 | 복잡한 도메인 객체 |
Functional Builder | • 극도의 유연성 • 확장 메서드 활용 | • 성능 오버헤드 • 타입 안전성 부족 | 동적 구성이 필요한 경우 |
💡 실제 활용 사례
1. StringBuilder (표준 라이브러리)
1
2
3
4
var sb = new StringBuilder()
.Append("Hello")
.Append(" ")
.AppendLine("World!");
2. LINQ 쿼리
1
2
3
4
var query = dbContext.Users
.Where(u => u.Age > 18)
.OrderBy(u => u.Name)
.Take(10);
3. ASP.NET Core 설정
1
2
3
4
5
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
4. HTTP 요청 빌더
1
2
3
4
5
6
var request = new HttpRequestBuilder()
.SetMethod(HttpMethod.Post)
.SetUrl("https://api.example.com/users")
.AddHeader("Authorization", "Bearer token")
.SetBody(jsonContent)
.Build();
✅ 빌더 패턴 사용 지침
언제 사용할까?
- 생성자 매개변수가 4개 이상일 때
- 선택적 매개변수가 많을 때
- 객체 생성 과정에서 복잡한 검증이 필요할 때
- 불변 객체를 생성해야 할 때
- DSL(Domain Specific Language)을 구현할 때
언제 피해야 할까?
- 단순한 객체 (매개변수 3개 이하)
- 모든 필드가 필수인 경우
- 성능이 극도로 중요한 경우
핵심 정리
빌더 패턴은 복잡한 객체 생성을 단순화하고 코드의 가독성을 크게 향상시키는 강력한 패턴입니다.
핵심 이점:
- 가독성: 메서드 체이닝으로 자연어에 가까운 코드 작성
- 유연성: 선택적 매개변수를 우아하게 처리
- 유지보수성: 새로운 매개변수 추가가 용이
- 안전성: 객체 생성 과정에서 일관된 검증 가능
프로젝트의 요구사항과 복잡도에 따라 적절한 빌더 패턴 변형을 선택하여 사용하면, 유지보수가 용이하고 확장 가능한 코드를 작성할 수 있습니다.
This post is licensed under CC BY 4.0 by the author.