1. Builder pattern
빌더패턴은 객체 생성 디자인 패턴 중 하나로 복잡한 객체를 단계적으로 생성할 수 있도록 하는 설계 패턴이다. 빌더패턴을 사용하면 하나의 객체 생성 코드를 통해서 다른 타입과 표현 방식을 가지는 객체를 생성할 수 있다.
- why Builder pattern?
어떤 객체를 생성해야 한다고 가정해보자. 우리는 이 객체를 생성하기 위한 필드들과 로직을 포함한 생성자를 구현하여 객체를 생성할 수 있다. 하지만 만약에 이 객체에 확장이 필요하다면 어떨까? 객체에 필요한 필드들이 늘어나고 복잡한 로직들이 추가되어야 할 것이다.
이러한 문제를 해결할 수 있는 가장 간단한 방법은 서브클래스를 만드는 것이다. 부모 클래스와 자식 클래스를 나누어서 필요한 파라미터 조합들에 대하여 서브클래스를 만들어서 운영하는 것이다. 하지만 이 방법도 결국에는 너무 많은 서브클래스를 만들거나 복잡한 로직을 가지게 되는 클래스를 만들게 될 수 있다.
많은 수의 서브클래스를 운영하기 어렵다면 하나의 생성자가 모든 필드 값을 파라미터로 입력받도록 하여 구현할 수 있다. 그렇다면 더이상 자식 클래스가 필요하지 않을 것이다. 하지만 이때, 옵션인 파라미터 값들이 많다면 중요한 파라미터들만 계속해서 사용되고 나머지 파라미터들은 특별한 경우에만 사용될 것이다. 이 경우에는 사용하지 않는 파라미터의 타입과 순서에 대한 관리가 어려워지게 된다. 또한 매사용마다 null 과 같은 기본값을 넘겨주어야 하고, 서브클래스가 무거워지는 문제가 발생한다. 그렇기 때문에 모든 필드에 대한 파라미터를 가지는 생성자를 사용하는 것은 코드를 복잡하게 만드는 좋지 않은 상황이 될 수 있다.
2. Builder pattern 구성
빌더패턴은 Builder, ConcreteBuilder, Director, Product 4 가지로 구성되어 있다. 각각의 역할은 다음과 같다.
Builder: Product 객체의 일부 요소들을 생성하기 위해 추상 인터페이스를 정의한 클래스
ConcreteBuilder: Builder 클래스에 정의된 인터페이스를 구현한 클래스. Product 의 각 요소를 조합하여 결과를 만들어 내기 위하여 각 요소를 정의하고 관리한다.
Director: Builder 인터페이스를 사용하는 로직을 가지는 객체를 정의하는 클래스
Product: Builder 패턴을 사용하여 결과물로 생성할 객체를 정의하는 클래스
빌더패턴은 Product 의 생성 코드를 해당 클래스에서 빼서 따로 분리된 Builder 객체에 포함시키도록 한다. 해당 패턴은 객체 생성을 단계로 나누어서 구성한다. 객체를 생성하기 위해서는 사용자가 Builder 클래스에서 원하는 단계의 메서드들을 호출하여 객체를 생성할 수 있다.
Builder 클래스로 Product 를 생성할 때 모든 단계를 호출하여 값을 입력하지 않아도 된다. 오직 필요한 필드에 해당하는 메서드들만 호출하여 객체를 구성할 수 있다. 이를 통해서 필수적인 파라미터들만 생성자에 추가하고 옵션값들은 메서드를 통해서 입력받는 방식도 가능하다.
빌터패턴에서는 디렉터 클래스를 구현하여 클라이언트 코드를 더 간결하게 유지할 수 있다. 디렉터 클래스는 특정 객체를 생성하는데 사용하는 빌더 단계 들의 메서드 호출을 미리 구현한 클래스이다. 사용자는 미리 빌더 단계를 구현해놓은 디렉터 클래스의 메서드를 호출함으로써 굳이 모든 단계를 직접 구현하지 않고도 객체를 생성할 수 있다.
3. Example
아래는 Table 을 생성하는 예제 코드를 builder 패턴을 적용하여 구현한 것이다.
public class Table {
private UUID id;
private String name = "table";
private int width = 1;
private int depth = 1;
private int length = 1;
public Table() {
this.id = UUID.randomUUID();
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getDepth() {
return depth;
}
public void setDepth(int depth) {
this.depth = depth;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
return "Table [id=" + id + ", name=" + name + ", width=" + width + ", depth=" + depth + ", length=" + length
+ "]";
}
}
첫번째 코드는 Table 클래스이다. 이 코드는 우리가 빌더패턴을 적용한 로직으로 생성하고자 하는 객체로 빌더패턴의 구성 요소 중에서 Product 를 의미한다. Table 은 id, name, width, depth, length 등의 필드를 가지고 있다.
public interface Builder {
public Builder reset();
public Builder setName(String name);
public Builder setWidth(int width);
public Builder setDepth(int depth);
public Builder setLength(int length);
public Builder setLeg(int leg);
public Product getResult();\
}
두번째 코드는 Builder 인터페이스이다. 이 코드는 Builder 인터페이스를 구현한 것이다. Product 를 만들기 위한 추상 인터페이스로 Product 생성에 필요한 메서드들을 선언해놓았다. ConcreteBuilder 역할의 클래스가 해당 인터페이스를 구현하여 실제 객체 생성 시에 동작하게 된다.
public class TableBuilder implements Builder {
private Table table = null;
@Override
public Builder reset() {
table = new Table();
return this;
}
@Override
public Builder setDepth(int depth) {
table.setDepth(depth);
return this;
}
@Override
public Builder setLength(int length) {
table.setLength(length);
return this;
}
@Override
public Builder setName(String name) {
table.setName(name);
return this;
}
@Override
public Builder setWidth(int width) {
table.setWidth(width);
return this;
}
@Override
public Table getResult() {
return table;
}
}
세번째 코드는 TableBuilder 클래스로 ConcreteBuilder 의 예제코드이다. 앞서 정의한 Builder 인터페이스를 구현한 클래스로 실제 Table 을 생성하는 ConcreteBuilder 역할의 클래스이다. Builder 인터페이스에서 선언한 각 메서드들을 구현해놓았다.
public class TableDirector {
public void constructDefaultTable(TableBuilder builder) {
builder.reset().setName("default table");
}
public void constructBigTable(TableBuilder builder) {
builder.reset().setName("big table").setLength(5).setDepth(5).setWidth(5);
}
}
네번째 코드는 빌더패턴에서 Derector 역할을 하는 코드인 TableDirector 클래스이다. default table 을 만드는 메서드와 big table 을 만드는 메서드를 구현하였다. 각각의 table 특성에 맞게 필요한 메서드들을 호출하도록 하였다.
default table 의 경우에는 name 만 정의하고 length, depth, width 는 기본값으로 정의한 반면에 big table 의 경우에는 기본값보다 큰 값들을 입력하여 Table 객체를 생성하도록 하였다.
public class TableBuilderTest {
@Test
public void testBuilderTest() {
TableBuilder builder = new TableBuilder();
TableDirector director = new TableDirector();
director.constructBigTable(builder);
Table bigTable = builder.getResult();
System.out.println(bigTable);
director.constructDefaultTable(builder);
Table defaultTable = builder.getResult();
System.out.println(defaultTable);
}
}
테스트 코드에서는 TableBuilder 와 TableDirector 객체를 생성하여 Table 객체를 만들도록 하였다. 앞서 Director 에서 미리 정의해놓은 Table 생성 가이드 메서드를 사용하여 builder 에서 Table 의 값들을 입력받도록 하였다.
테스트 코드의 실행 결과는 다음과 같다.
Table [id=b2e7ccb6-bd22-4540-b67c-f1a3ef59c7f5, name=big table, width=5, depth=5, length=5]
Table [id=d2eb3cd9-880a-477a-a18f-fa7ca0010ffc, name=default table, width=1, depth=1, length=1]
[reference]
- https://refactoring.guru/design-patterns/builder
- https://dev-youngjun.tistory.com/197
'Computer Science > Design Pattern' 카테고리의 다른 글
[Design Pattern] Abstract Factory pattern (0) | 2023.01.21 |
---|