[Design Pattern] Builder

[Design Pattern] Builder

[Design Pattern] Builder

제가 느낀 빌더(Builder) 패턴의 키워드는 “객체 구축”입니다.

Builder Pattern

빌더 패턴은 복잡한 객체 생성을 한 번의 생성자 호출로 만드는 대신 생성 과정을 여러 단계로 나누어 각 구성 요소를 점진적으로 설정하고 조립하여 최종 객체를 완성하도록 하는 패턴입니다.

빌더 패턴은 크게 두 가지 형태가 있습니다. GoF에서 정의한 Builder 패턴과 Joshua Bloch가 Effective Java에서 소개한 Builder 패턴이 존재합니다. 두 패턴은 이름은 같지만 구조와 사용 목적에는 차이가 있습니다.

실무에서 “빌더 패턴”이라고 이야기하면 보통 Joshua Bloch가 소개한 방식을 의미합니다.


GoF Builder Pattern

디자인 패턴의 원형으로 복잡한 객체의 “조립 절차” 자체를 분리하는 데 목적이 있습니다.

예를 들어 다음과 같은 상황을 생각해 보겠습니다.

  • 객체를 여러 단계에 걸쳐 조립해야 합니다.
  • 구성 순서가 결과에 영향을 미칩니다.
  • 같은 조립 절차를 재사용해야 합니다.
  • 조립 방식에 따라 서로 다른 형태의 결과물이 만들어집니다.

이런 경우 생성 로직이 한 클래스에 모두 들어가게 되면, 생성 코드가 너무 비대해지고 변경에 취약해집니다. 이런 문제를 해결하기 위하여 조립 절차(Director)와 구체적인 구성 내용(Builder)으로 분리합니다.


패턴 구조

빌더 패턴 다이어그램

  • Builder
    • 객체를 구성하는 단계들을 정의하는 추상 계층.
    • “어떤 단계가 존재하는가?”를 결정.
  • ConcreteBuilder
    • 실제 구성 내용을 담당.
    • “각 단계가 어떻게 만들 것인가?”를 결정.
  • Director
    • 조립 순서를 제어하고 동일한 생성 절차로 서로 다른 결과물을 만들 수 있도록 함.
    • “어떤 순서로 만들 것인가?”를 결정.
  • Client
    • ConcreteBuilderDirector에 제공하여 객체 생성을 함.
    • 객체 생성 절차를 몰라도 됨.

구현 예시

  • 상황 예시
    • 같은 데이터를 기반으로 TEXT 문서와 JSON 문서를 만들어야 한다고 가정하겠습니다.
  • Data - 공통 데이터 구조
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      public class Data {
          public String name;
          public Integer age;
    
          public Data(String name, Integer age) {
              this.name = name;
              this.age = age;
          }
      }
    
  • Builder
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      public abstract class Builder {
          Data data;
    
          public Builder(Data data) { this.data = data; }
    
          public abstract String buildHead();
          public abstract String buildName();
          public abstract String buildAge();
          public abstract String buildTail();
      }
    
  • Director
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      public class Director {
          Builder builder;
    
          public Director(Builder builder) { this.builder = builder; }
    
          public String construct() {
              StringBuilder sb = new StringBuilder();
    
              sb.append(builder.buildHead());
              sb.append(builder.buildName());
              sb.append(builder.buildAge());
              sb.append(builder.buildTail());
    
              return sb.toString();
          }
      }
    
  • JsonBuilder
    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
    
      public class JsonBuilder extends Builder{
          public JsonBuilder(Data data) {
              super(data);
          }
    
          @Override
          public String buildHead() {
              return "{\n";
          }
    
          @Override
          public String buildName() {
              return "\tname: " + data.name + ", \n";
          }
    
          @Override
          public String buildAge() {
              return "\tage: " + data.age + "\n";
          }
    
          @Override
          public String buildTail() {
              return "}";
          }
      }
    
  • TextBuilder
    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
    
      public class TextBuilder extends Builder{
          public TextBuilder(Data data) {
              super(data);
          }
    
          @Override
          public String buildHead() {
              return "Let me introduce myself. ";
          }
    
          @Override
          public String buildName() {
              return "My name is " + data.name;
          }
    
          @Override
          public String buildAge() {
              return " and my age is " + data.age + ". ";
          }
    
          @Override
          public String buildTail() {
              return "Nice to meet you.";
          }
      }
    
  • Client
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      class Main {
    
          public static void main(String[] args) throws Exception {
    
              Data data = new Data("admin", 25);
    
              // Text format
              Director textDirector = new Director(new TextBuilder(data));
              System.out.println(textDirector.construct());
    
              // Json format
              Director jsonDirector = new Director(new JsonBuilder(data));
              System.out.println(jsonDirector.construct());
          }
      }
    

이 처럼 Director 통하여 일관된 프로세스로 객체를 생성하고 재사용할 수 있게 됩니다.


Bloch Builder Pattern

해당 패턴은 “파라미터가 많은 생성자 호출이 가져오는 가독성과 안정성 문제”를 해결하는 데 초점을 두고 있습니다.

예를 들어 다음과 같은 생성 코드가 있다고 가정해 보겠습니다.

1
new Something(a, b, c, d, e, ...);

해당 방식에는 여러 단점이 존재합니다.

첫째, 각 파라미터의 의미가 코드만 보고는 명확하게 드러나지 않습니다.
변수명을 직접 보지 않는 이상 어떤 값이 어떤 역할을 하는지 한눈에 파악하기 어렵습니다.
둘째, 파라미터 순서가 바뀌는 실수를 유발하기 쉽습니다.
특히 타입이 같은 파라미터가 여러 개일 경우 컴파일 단계에서 오류가 발생하지 않기 때문에 런타임 버그로 이어질 가능성이 큽니다.
셋째, 기본값을 가지는 파라미터라도 생성마다 직접 전달해야 하는 불편함이 있습니다.
생성자 다형성을 통하여 구현할 수 있지만 생성자 시그니처가 계속 늘어나는 문제로 이어집니다.

생성자 종류가 과도하게 많아지면서 어떤 생성자를 사용해야 하는지 판단하기 어려워지는 문제를 “생성자 폭발 문제”라고 합니다.

구현 예시

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
public class Article {
    private String title;
    private String content;
    private String author;

    public static class Builder {
        private String title;
        private String content;
        private String author;

        Builder title(String title) {
            this.title = title;
            return this;
        }

        Builder content(String content) {
            this.content = content;
            return this;
        }

        Builder author(String author) {
            this.author = author;
            return this;
        }

        Article build() {
            return new Article(this);
        }
    }

    // 빌더를 사용하여 생성하기에 private 설정.
    private Article(Builder builder) {
        this.title = builder.title;
        this.content = builder.content;
        this.author = builder.author;
    }
}

public class Client {
    void use() {
        Article article = new Article.Builder()
                .title("test")
                .content("content")
                .author("bienew22")
                .build();
    }
}


참고 문헌

This post is licensed under CC BY 4.0 by the author.