본문 바로가기
디자인 패턴

빌더 패턴 + [F]

by oncerun 2022. 2. 19.
반응형

 

빌더 패턴은 생성 패턴(Creational Patterns)중 하나로 오브젝트의 생성에 관련된 패턴 중 하나이다.

 

  • 객체의 생성에 대한 로직과 표현에 대한 로직을 분리
  • 객체의 생성과정을 유연하게 해 준다.
  • 객체의 생성과정을 직접 정의하고 싶거나 생성자가 복잡한 경우

Car Class에 빌더 패턴을 적용해보자.  

public class Car {

    private long id;
    private String name;
    private String brand;
    private BigDecimal distanceDriven;
    private boolean autopilot;
    
}

 

기본 생성자로 객체를 정의하는 경우 우리는 빈 객체를 생성한 후 필드에 값을 주입하여 사용했다. 

그런데 생성된 객체의 필드가 변경되지 않는 경우는 setter를 정의하지 않고 객체를 생성할 때 모든 필드 값을 주입하여 생성 이후 값이 변경되지 않도록 할 때가 존재한다.  우리는 이를 immutable 한 object라고 한다.

 

immutable 한 객체를 생성하는 방법은 생성자에 모든 값을 정의한 후 객체를 생성해야 하는데, 해당 객체의 필드가 많을 경우 생성자는 매우 복잡해진다. 

 

immutable 해야 하고, 생성 과정이 유연해야 하고 복잡하지 않으면서 생성과 표현의 로직을 분리해야 한다. 

 

그럼 우리는 생성에 대한 책임을 가지는 객체 하나를 생각해볼 수 있다. 이 객체의 클래스 이름은 Builder이다.

 

Builder Class는 Car의 객체의 생성 책임을 가지는 객체이며 그에 맞는 행동을 정의해야 한다. 

 

public static class Builder {
    private long id;
    private String name;
    private String brand;
    private BigDecimal distanceDriven;
    private boolean autopilot;

    private Builder(long id) {
        this.id = id;
    }

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

    public Builder withBrand(String brand){
        this.brand = brand;
        return this;
    }

    public Builder withDistanceDriven(BigDecimal distanceDriven) {
        this.distanceDriven = distanceDriven;
        return this;
    }

    public Builder withAutopilot(boolean autopilot) {
        this.autopilot = autopilot;
        return this;
    }

    public Car build() {
        return new Car(this);
    }
}

Builder의 생성자는 private으로 외부에서 Builder의 인스턴스를 생성하지 못한다. 

 

Builder의 필드 값은 Car 인스턴스의 필드 값으로 주입되어야 한다.

public Car(Builder builder) {
    this.id = builder.id;
    this.name = builder.name;
    this.brand = builder.brand;
    this.distanceDriven = builder.distanceDriven;
    this.autopilot = builder.autopilot;
}

 

Builder는 Car의 인스턴스를 생성하여 반환하는 로직이 필요하다.

public Car build() {
    return new Car(this);
}

 

반대로 Car는 자신을 생성할 수 있게 Builder를 반환하는 static 메서드가 필요하다.

public static Builder builder(long id){  //id는 필수 필드값
    return new Builder(id);
}

 

이제 Builder를 사용해 Car의 인스턴스를 생성해보자.

Car car = Car.builder(1L)
        .withAutopilot(true)
        .withBrand("Tesla")
        .withName("Model S")
        .withDistanceDriven(BigDecimal.ZERO)
        .build();

 

Car는 자신의 생성을 Builder에게 위임함으로 써 책임을 덜고 Builder Class는 Car의 생성을 책임지면서 기존 생성자를 통한 생성에 비해 조금 더 직관적으로 Car의 인스턴스를 생성했다. 

 

이로 인해 Car의 인스턴스의 초기화 과정을 책임지는 역할 또한 Builder로 넘어가게 되면서 생성에 유연성을 가지게 되었다.  하지만 with + "FieldName"을 호출하는 부분이 많다면 가독성이 좋다고 하기엔 조금 미숙한 부분이 존재한다.

 

기존 우리가 아는 Setter의 모양은 다음과 같다.

public void setName(String name) {
    this.name = name;
}

 

이는 반환 타입은 void이면서 필드 타입에 대한 인자를 하나를 받는다. 

마치 Funtional interface의 Consumer <T>와 하는 행동이 같다.

그렇다면 우리는 Consumer를 이용해 setter를 하나로 줄일 수 있지 않을까? 

 

이를 위해선 Builder의 주입할 필드의 접근자를 private -> public으로 변경해야 한다.

public static class Builder {
    private long id;
    public String name;
    public String brand;
    public BigDecimal distanceDriven;
    public boolean autopilot;
    
    public Builder with(Consumer<Builder> consumer) {
        consumer.accept(this);
        return this;
    }

	...
}

 

consumer를 통해 Car 객체를 생성해보자.

 

Car car = Car.builder(1L)
            .with(builder -> {
                builder.name = "Model A";
                builder.autopilot = true;
                builder.distanceDriven = BigDecimal.valueOf(103.23);
                builder.brand = "Tesla";
            })
            .build();

 

기존의 생성자를 통해 객체를 생성하는 방법에서 빌더 패턴을 통해 얻은 장점은 다음과 같다.

 

  • 객체의 생성에 필요한 로직과 표현에 대한 로직을 분리하였다. 

  • 객체의 생성과정을 유연하게 설정이 가능하도록 변경되었다. 
    객체의 필드 추가에 대해서 객체 생성 코드를 모두 추적해  코드를 변경하고 싶지는 않다.
    단지 객체 생성 관련 코드를 Builder에 모으는 것만으로도 한 곳에서 관리할 수 있다.

  • 가독성이 좋아졌다.
    가독성이 좋다 안 좋다는 판단은 사람마다 다르지만 함수형 프로그래밍을 접하거나 빌더 패턴을 접한 사람에게는 가독성이 좋아 보일 것이고 생성자를 통해 객체를 생성한 사람에게는 가독성이 안 좋아 보일 수 있다.
    이는 개발하는 팀원과 문화에 따라 느끼는 점이 다른 것 같다.

  • 불변성을 해치려는 다른 개발자가 한번 더 생각할 수 있는 코드를 제공한다.
    Car객체가 이리저리 사용될 때 생성 부분이 아닌 비즈니스 로직 부분에서 값을 중간에 세팅하려고 할 때 내부 코드를 보고 setter를 추가하지 않을 가능성이 있다. 
    아 물론 가능성이다....
    사실 중간에 값을 변경해야 하는 일이 존재한다면 이는 클래스 설계가 잘못된 것이 맞다고 생각한다.  
    하지만 의도가 immutable 한 object로 사용되는 것이 맞다면 다른 개발자가 해당 코드를 보고 이해 혹은 질문을 통해 코드를 리팩터링 할 수 있을 것이라는 생각이다.

 

 

반응형

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

책임 연쇄 패턴 +[F]  (0) 2022.02.19
데코레이터 패턴 + [F]  (0) 2022.02.19
데코레이터 패턴  (0) 2022.01.21
프록시 패턴  (0) 2022.01.20
전략 패턴  (0) 2022.01.17

댓글