Skip to main content

Builder Pattern

Overview​

동일한 프로세스로 객체를 생성할 수 있게하는 패턴. Builder pattern overview diagram

Why to use​

일관된 클래스 생성 규칙과 프로세스로 가독성 있는 코드로 객체 생성 방식 정립.

기존 생성자 방식의 한계​

매우 긴 생성자를 생성하거나, 간단한 생성자로 인스턴스 생성후 장황하게 setter 로 만들면 일괄된 프로세스로 객체를 생성하지 못하므로 실수할 위험이 증가함.
ex) 5박 6일이면 '6일'만 구하거나 '5박'만 세팅하면 안되고 강제해야함
setter 를 사용하면 안됨. => 인스턴스가 불완전한 프로세스로 만들어질 위험이 있고
다양한 파라미터를 지원하는 생성자를 만들 때 너무 김.

  1. 생성자 생성시 너무 길고 장황하다.
  2. 불완전한 객체 생성 프로세스
    객체 생성 프로세스를 강제할 수 없다.
    ex) 여행의 경우 Days, nights, startDate, endDate 는 반드시 필요하고
    나머지 속성은 Optional 이라고 가정하자.
    해당 파라미터들만 받는 생성자를 만드는 방법도 있지만
    각 속성별로 변경하는 커스텀 메소드를 나눠놨다고하면 이를 강제하기 까다롭다.
caution

위와 같은 상황에서는 매우 여러 생성자를 정의해야하고 내부 로직도 복잡해지기 쉽다.

When to use​

  • 복잡한 객체를 만드는 프로세스를 정립하고 독립적으로 분리해야할 때
  • 객체 생성 프로세스 로직이 복잡할 때
  • 많은 생성자를 정의해야 할 때
  • 객체 생성시 생성자에 필요한 파라미터가 많아질 때

How to use​

인스턴스 만드는 방법을 Builder 인터페이스를 정의하고 step 별로 나눠 프로세스를 정립한다.

  1. 빌더 Interface 정의
  2. 빌더 구현체 정의
  3. Director 정의 (Optional)

Example​

카직스

TourPlan.java​

Product class 정의

@Getter
@NoArgsConstructor(access = AccessLevel.PUBLIC)
// Don't recommend input al fields into constructor
@AllArgsConstructor(access = AccessLevel.PUBLIC)
public class TourPlan {
private String title;
private int nights;
private int days;

private LocalDate startDate;
private LocalDate endDate;
private String stayPlace;
private List<DetailPlan> plans = new ArrayList();

// nights - days 는 별도로 지정하지 않아도
// startDate - endDate 연산 만으로 쉽게 구해진다.
public void changeDays(int days) {
if (days <= 0) {
throw new IllegalArgumentException("days should be greater than 0");
}

this.nights = days - 1;
this.days = days;
}

// 필수 지정 메소드
public void changeDate(LocalDate startDate, LocalDate endDate) {
this.startDate = startDate;
this.endDate = endDate;
}

public void changeStayPlace(String stayPlace) {
this.stayPlace = stayPlace;
}

@Override
public String toString() {
return "title : " + this.title + "\n" +
"start date: " + this.startDate + "\n"
+ "endDate: " + this.endDate;
}
}

별도로 Builder, Director 클래스 정의없이 빌더를 사용할 수 있어 매우 간단하다.
그러나 이 방법은 @AllArgsConstructor 어노테이션을 사용해야하므로 권장되지 않는다.

Spring 활용 예​

UriComponentBuilder 로 URI 를 안전하게 생성할 수 있다.

public class SpringCreatePatternTest {
@Test
void uriComponentTest() {
var uriComponents = UriComponentsBuilder.newInstance()
.scheme("https")
.host("www.m-falcon.tistory")
.path("656")
.build()
.encode()
.toUriString();

var designPatternUri = "https://www.m-falcon.tistory/656";

assertThat(uriComponents).isEqualTo(designPatternUri);
}
}

Question​

Required / Optional 나눌 수 있을까?

build() method 에서 null check 하는 방법보다 뭔가 더 fancy 한 방법이 있을 것 같다.

Pros and Cons​

장점​

  • 인스턴스를 생성하는 프로세스 강제
    단순 생성자로 인스턴스를 만들 때에는 일련의 체계가 없어 실수하기 쉬움.
    생성자에 validation 로직 삽입시 복잡도 증가 불가피

  • Director 클래스를 통해 복잡한 객체를 만드는 과정을 Wrapping 할 수 있음.
    별도의 Builder 를 커스텀하여 동일한 프로세스를 거치지만 다른 인스턴스를 만들 수도 있게함 (확장성)

단점​

  • 성능 면에서 약간의 오버헤드가 발생
    생성하고자하는 인스턴스 생성자가 아닌 builder 를 경유해야 하므로 추가 인스턴스 생성으로 인한 약간의 오베헤드 발생
  • 구조상 복잡도 증가
    Builder 또는 Director 등 별도의 인터페이스 및 클래스 정의 필요.

🔗 Reference​

코딩으로 학습하는 GoF의 디자인 패턴 - 백기선