빌더 패턴은 무엇일까? 무엇을 조심해야 할까? 어떤식으로 활용하는 것이 좋을까?
Builder
- 개체의 생성과정을 그 개체의 클래스로부터 분리하는 방법
- 개체의 부분부분을 만들어 나가다 준비가되면 그제서야 개체를 생성
StringBuilder
- 단순한 것은 이걸 사용해서 원하는 문자열을 만들 수 있다.
- 하지만 복잡해진다면?
- 예를 들어 하나의 String에 100개의 변수를 넣어 String을 만들어야 한다면?
StringBuilder builder = new StringBuilder(4096);
builder.append(heading);
builder.append(newLine);
builder.append(newLine);
builder.append(leadingParagraph);
builder.append(newLine);
for (KeyValue kv: data) {
builder.append(" * ");
builder.append(kv.getKey());
builder.append(": ");
builder.append(kv.getValue());
builder.append(newLine);
}
String document = builder.toString();- 이럴 때
StringBuilder를 사용하면 편하다. - overloading된 메서드가 있어서 다양한 자료형도 받을 수 있다.
- 심지어 성능도 더 빠르다. 알아서 효율적으로 합쳐준다.
- 다만 내부가 어떻게 도는지 알면 더 좋음
- 마지막에
toString()을 통해 결과물을 가져옴
문제점
- 코드가 잘 읽히나 2%부족
- 작성자의 의도: 제목을 넣고 줄을 바꾸고 싶다.
- 근데 저 위에 코드만 봐서는 독립적인 3개의 컴포넌트를 추가하는 느낌임
Fluent interface
- GoF에는 없음
- 최근에 builder 패턴의 발전된 방식
StringBuilder builder = new StringBuilder(1024 * 1024);
builder.append(heading)
.append(newLine)
.append(newLine);
builder.append(leadingParagraph)
.append(newLine);
for (KeyValue kv: data) {
builder.append(" * ");
.append(kv.getKey());
.append(": ");
.append(kv.getValue());
.append(newLine);
}
String document = builder.toString();- 반환 타입을 자기자신을 넘겨주는 방식!
- (Walnut이 builder pattern으로 구현되었다는 것을 알 수 있었음)
자기 스스로를 반환한다고…?
- 함수 Signature만 봐서는 이상하다.
- 클라이언트 측에서 해당 개체를 들고 있는데 왜
return this를 하지? - 이상하지만 이제는 상식으로 취급된다.
- 클라이언트 측에서 해당 개체를 들고 있는데 왜
잘못 사용하는 빌더 패턴
public final class Employee {
private String firstName;
private String lastName;
private int id;
private int yearStarted;
private int age;
public Employee(String firstName, String lastName, int id, int, yearStarted, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.id = id;
this.yearStarted = yearStarted;
this.age = age;
}
}
Employee wansik = new Employee("Wansik", "Choi", 1, 1995, 28);
Employee howon = new Employee("Kim", "Howon", 30, 1993, 2);- 직원 정보를 입력하는 클래스가 있다고 해보자.
- 근데 두번째는 문제가 있다.
- 성과 이름을 바꿔서 넣었다. 그리고 나이와 Id도!
- 이런 경우는 당연히 컴파일러가 잡아줄 수 없다.
- 이런 것을 builder로 해결해보자.
Employee howon = new EmployeeBuilder(1)
.withAge(30)
.withStartingYear(1993)
.withName("Howon", "Kim")
.build();- 이렇게 하면 메서드 이름으로 넣으니 잘못된 값을 전달할 가능성이 적어진다.
- 하지만, 이건 잘못된 해결법
만약에 실수한다면?
Employee howon = new EmployeeBuilder(1)
.withAge(30)
.withName("Howon", "Kim")
.build();- 자바에서는 초기값이 0이다. 따라서 0년부터 일한 직원이 된다.
- 개체는 생성부터 유효한 상태여야 하는데 이를 어겼다.
- 이건 디자인 패턴을 잘못 사용한 경우다.
- 근데 이런식으로 만든 SDK가 많아졌다..
- G모사조차도..
StringBuilder는 유효한 개체만 반환했다.String만 다뤘기 때문에, 언제 호출해도 유효한String개체가 나온다.
해결 방법 1
- 파라미터를 빼먹지 않으면 되는 문제다.
- 그렇기 때문에 파라미터 자체를 가지는 다른 클래스를 만들어서 관리하자.
public final class Employee {
private String firstName;
private String lastName;
private int id;
private int yearStarted;
private int age;
public Employee(CreateEmployeeParams params) {
this.firstName = params.first;
this.lastName = params.lastName;
this.id = params.id;
this.yearStarted = params.yearStarted;
this.age = params.age;
}
}
CreateEmployeeParams params = new CreateEmployeeParams();
params.firstName = "Howon";
params.lastName = "Kim";
params.id = 1;
params.age = 30;
params.yearStarted = 1993;
Employee howon = new Employee(params)- 순수한 데이터만 담은 구조체처럼 만들어서 전달하는 방식이다.
- swift라면 당연히 struct로 만들겠지
- DTO
- 잘못 사용한 Builder 패턴 보다는 이게 좀 더 명확하고 좋다.
- 인자 순서(firstName, lastName) 잘못 넣는 문제는 수정할 수 있다.
- 근데 그나마 낫다는 거지 여전히 인자를 빼먹을 수 있다는 문제가 있다.
- 굳이
with~,build()호출하고 하는 방식을 쓸 이유가 없다는 말을 하고 싶은 것
해결 방법 2
- named parameter
- 인자에 이름을 달아버리는 방법
- 위의 모든 문제를 고칠 수 있다.
- Swift에서 변수 인자를 명시적으로 보여주는 것을 말한다.
Employee(firstName: "Howon", lastName: "Kim", id: 1, yearStarted: 1993, age: 30);정리
- 위와 같이 언어 자체에서 지원해줘야 하는 것들이 있다.
- 언어에서 지원하지 않는 기능(named parameter)때문에 builder를 사용한다면 꼼수로 사용하는 것이다.
- builder pattern은, 어느 상태에서 적용해도 개체의 유효성이 보장될 경우에 사용해야 한다.
- 예를 들어,
UIView에 다양한 요소를 넣을 수 있지만, 넣지 않아도UIView자체가 의미있는 경우가 있겠다.
다형적인 빌더 패턴
.csv파일이 있다고 생각해보자.- 이걸 HTML, markdown으로 각각 변환하고 싶다.

CsvReader reader = new CsvReader(csvText);
HtmlTableBuilder builder = new HtmlTableBuilder();
reader.writeTo(builder);
HtmlDocument html = builder.toHtmlDocument();CsvReader reader = new CsvReader(csvText);
MarkdownTableBuilder = builder = new MarkdownTableBuilder();
reader.writeTo(builder);
String mdText = builder.toMarkDownText();- CsvReader에 문자열을 넣어준다.
- 어떤 타입의 Builder를 만들지 선택
- CsvReader는 같은 타입에서 처리해버린다. 각각의 Builder에서 처리 방법만 다를뿐이다. (다형적 호출)
- 마지막으로 builder에서 CsvReader에서 명령한 결과를 조합하여 결과를 얻는다. (다형적 호출 X)