본문 바로가기
디자인 패턴

Gof 생성 디자인 패턴 : Builder

by oncerun 2022. 11. 19.
반응형

최근 객체 생성하는 일이 잦아지면서 반복적이고 영향을 쉽게 받는 객체를 생성하고 있다는 생각이 많이 들었다. 

 

그래서 회사에서 디자인 패턴 책도 빌리고 관련 강의도 구매해 들으면서 생성 디자인 패턴 관련해 지식을 공부하면서 

책과 강의에서 요구하는 그 조건에 맞추지 않고 디자인 패턴들의 개념을 습득하여 내 상황에 맞게 적용하고 싶었다. 

 

3년 전 학원에서 디자인 패턴을 공부한 다했을 때 선생님이 지금은 너무 이르다고 일을 하다 보면 디자인 패턴을 공부하게 되는 적절한 타이밍이 올 거라고 했는데, 지금이 그 타이밍인 것 같다. 

 

디자인 패턴 중 행동, 구조 패턴들은 여러 객체를 조립하고 객체의 책임을 분리하고 유연하게 사용하는 내용은 뭐랄까

이론만으로는 쉽게 체득되지 않았다.  

 

그래서 생성 디자인 패턴은 이론을 공부하고 실습하면서 조금 복잡한 데모 코드를 만들어 보면서 공부할 예정이다. 

 

 

빌더 패턴

 

생성 패턴으로 빌더 패턴을 처음으로 공부하는 이유는 다음과 같다. 

Entity를 생성하는 과정에 대한 고민이 많았다. 엔티티의 생성자의 범위를 public 하게 노출시키고 싶지 않아 기본 생성자를 막고 필요한 필드만 노출함으로써 객체를 생성하고 싶었다. 

그리고 엔티티 값 설정을 위해 엔티티와 DTO의 관계를 끊어버리고 싶었고 생성 책임을 DTO에게 넘기고 싶었다.

또한 엔티티의 연관관계도 커스텀하는 코드가 필요했다. 

이 과정에서 적절하게 사용할 수 있는 패턴이 뭔지 열심히 질문하고 찾아봤다. 

이 문제를 해결할 수 있는 방안 중 하나는 빌더 패턴이다. 

 

의도 

 복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공할 수 있도록 한다. 

 

 

책에서 빌더 패턴을 사용하게된 동기는 다음과 같다. 

 

특정 문서 판독기는 특정 포맷을 읽고 다른 포맷으로 변경할 수 있어야 한다. 

- 벌써 판독기는 두 가지 책임을 가지고 있다. 읽고, 변환까지 

그런데 다른 포맷 타입의 변경에 제약사항이 없어 변환 기능의 추가는 지속적인 요구사항이 될 수 있다. 하지만 판독기는 변화에 영향을 받으면 안 되는 상황이다.

 

그래서 판독기와 변환기로 분리하였다. 이를 복합하여 사용해야 했는데 지속적인 기능 요청사항은 변환기에 있어 여려 자식 클래스가 생성될 수 있다. 

 

이러한 변환기 클래스들은 복잡한 객체를 생성하고 조립하는 데 필요한 메커니즘을 인터페이스에 정의해 각 연산을 구현한다. 

 

여기서 판독기는 디렉터(director)라고 하고 변환기 클래스들을 우리는 builder 라고 합니다. 

 

 

 

위 예제를 정리하면 동일한 프로세스를 거쳐서 다양한 구성의 인스턴스를 만들려고 하는 것입니다.

 

객체의 생성이 객체를 합성하는 요소 객체들이 무엇인지 이들의 조립 방법에 독립적이고 합성 객체들의 표현이 다르더라도 이를 생성 절차에서 이를 지원할 때 사용할 수 있습니다. 

 

책의 내용은 외국의 개발 용어 기준으로 설명되어 있어 독해에 조금 난감한 부분이 있습니다. 

 

이해하기 위해 데모코드를 작성해 보겠습니다.

 

  • 우리는 Builder 인터페이스에서 인스턴스를 만드는 방법들을 정의할 것입니다. 
  • 그리고 만들어진 결과를 받을 수 있도록 할 것입니다. 
  • 이때 빌더와 빌더 구현체는 인터페이스를 통한 관계로 여러 구현체들이 추가될 수 있습니다.

 

 

public interface Builder {

    Builder name(String name);

    Builder age(int age);

    Builder location(String name);

    Product build();

}

 

우선  Product 객체의 일부 요소들을 생성하기 위한 인터페이스를 정의합니다. 

 

이때 메서드 체이닝을 통해 Product의 요소들을 저장하기 위해 Return 타입을 Builder 인터페이스로 지정합니다. 

 

마지막으로 해당 값을 통해 최종 Product를 제공해야 합니다. 

 

public class DefaultProductBuilder implements  Builder{

    private String name;
    private int age;
    private String location;

    @Override
    public Builder name(String name) {
        this.name = name;
        return this;
    }

    @Override
    public Builder age(int age) {
        this.age = age;
        return this;
    }

    @Override
    public Builder location(String location) {
        this.location = location;
        return this;
    }

    @Override
    public Product build() {

        return new Product(name, age, location);
    }
}

 

이제 객체의 생성에 필요한 요소들을 조립할 수 있는 구현체가 필요합니다.

DefaultProductBuilder를 생성하여 Product에 들어갈 수 있도록 준비해야 합니다.

 

이제 클라이언트 코드는 다음과 같이 구성될 것입니다.

 

public class Client {

    public static void main(String[] args) {
        Builder builder = new DefaultProductBuilder();
        Product product = builder
                .age(10)
                .name("name")
                .location("location")
                .build();
    }
}

 

우리는 이렇게 클라이언트 코드에서  Director에 빌더를 저장하여 재활용할 수 있습니다. 

 

public class Director {

    private DefaultProductBuilder builder;

    public Director(DefaultProductBuilder defaultProductBuilder) {
        this.builder = defaultProductBuilder;
    }

    public Product defaultProduct() {
        return builder
                .age(10)
                .name("name")
                .location("location")
                .build();
    }


}

 

만약 여러 구현체를 변경해가면서 Director를 사용하고 싶다면 Builder 타입으로 필드를 선언해 런타임에 원하는 빌더 구현체를 주입받아 사용하도록 할 수 있겠죠.

 

public class Client {

    public static void main(String[] args) {
        Director director = new Director(new DefaultProductBuilder());
        Product product = director.defaultProduct();
    }
}

 

이 코드가 바로 책에서 설명하는 협력 방법입니다.

 

1. 사용자는 Director 객체를 생성하고 이렇게 생성한 객체를 자신이 원하는 Builder 객체로 합성해 나간다. 

 

2. Product의 일부가 구축될 때마다 Director는 빌더에게 통보한다.

이때 책에선 생성자에 필요한 값을 전달하도록 되어있지만 위 예시에서는 값을 포함하여 재활용하기 위한 빌더 구현체를 전략에 따라 갈아 끼우도록 하였습니다.

 

3. Builder는 Director의 요청을 처리하여 제품에 부품을 추가한다. 

 

4. 클라이언트는 Product를 전달받는다. 

 

 

빌더의 장점과 단점

 

점층적인 생성자를 제거하기위해 빌더 패턴을 사용할 수 있다. 실제 필요한 단계들만 사용하여 단계별로 객체들을 생성할 수 있기 때문에 여러 오버로드된 생성자의 코드를 줄일 수 있다.

 

Product의 생성 과정 중 세부 사항만 다른 유사한 단계를 포함할 때 적용하면 효과적이다.

 

빌더 패턴을 통해 제품들을 순차적으로 생성할 수 있다. 또한 재귀적으로 단계를 호출할 수도 있는데, 이는 객체 트리를 구축해야 할 때 매우 유용하다.

 

단일 책임 원칙, 복잡한 생성 코드를 고립 시킬 수 있다.

 

클라이언트 코드는 빌더 객체와 디렉터를 사용한다면 디렉터 객체 모두 생성해야 한다. 

즉 클라이언트에서 디렉터에게 빌더 객체를 전달하는 코드가 노출된다.

 

만약 빌더 패턴이 여러 개라면 새 클래스들을 생성해야 하므로 복잡성 증가.

 

 

우리는 빌더 패턴을 Java의 StringBuilder, StringBuffer, Stream에서 볼 수 있습니다. 


StringBuilder stringBuilder = new StringBuilder();
Stream<String> stream = Stream.<String>builder().add("ga").add("na").build();

stringBuilder
        .append("add")
        .append(1)
        .toString();

 

혹은 Lombok을 사용하여 쉽게 적용할 수 있습니다.

 

 

 

 

반응형

'디자인 패턴' 카테고리의 다른 글

Gof 구조 디자인 패턴 : proxy  (0) 2022.11.20
Gof 행동 디자인 패턴 : State  (0) 2022.11.20
GoF 행동 패턴 소개 (1)  (0) 2022.11.15
GoF 구조적인 패턴 소개  (0) 2022.11.06
책임 연쇄 패턴 +[F]  (0) 2022.02.19

댓글