빌더 패턴

1. 빌더 패턴이란?

창작과 관련된 디자인 패턴으로, 같은 과정을 거쳐 다른 구성의 인스턴스를 만드는 방법

1-1. 정의

  • GoF 디자인 패턴 중 생성 패턴에 해당하다.
  • 빌더 패턴이 복잡함 객체를 생성하는 클래스그리고 표현하는 클래스깁스 분리그래서 같은 과정으로 다른 표현을 만드는 방법을 제공합니다.하다.
  • 생성할 객체에 선택적 속성이 많을수록 좋습니다.

2. 해결하고 싶은 문제

빌더 패턴은 생성과 함께 어떤 문제를 해결하려고 했습니까?

결론적으로 객체를 생성할 때 생성자만 사용할 때 발생할 수 있는 문제 개선하도록 설계된

이전에 본 생성 패턴과 비교하면, 팩토리 메소드 패턴이나 추상 팩토리 패턴에서 생성하고자 하는 클래스에 대한 속성값이 많을 때, 문제있다.

1. 클라이언트 프로그램에서 팩토리 클래스를 호출할 때 선택적 인수가 너무 많으면 유형 및 주문 관리가 어려움오류 발생 확률이 높아집니다.
2. 경우에 따라 매개변수가 필요하지 않음팩토리 클래스에 하나씩 없는 넘겨야 한다
3. 생성할 하위 클래스가 무겁고 복잡해짐에 따라 팩토리 클래스도 복잡합니다.일어난다.

Builder 패턴은 이러한 문제를 해결하기 위해 별도의 Builder 클래스를 만듭니다. 필수 값의 경우 생성자를 통해, 선택적 값의 경우 메서드를 통해 단계적으로 값을 입력한 후 build() 메소드를 통해 마지막으로 하나의 인스턴스를 반환하는 메서드입니다.

세 가지 주요 문제를 해결하려면 다음을 수행하십시오. 요구 사항만족해야합니다.

1. 불필요한 생성자를 만들지 않고 객체 생성
2. 데이터 어떤 순서로든 객체 생성
3. 이용자가 보는 경우 명백한그리고 이해할 수 있어야 합니다.

3. 코드로 알아보자

개발이 요구 사항을 기반으로 하는 상황을 가정합니다.

여행 계획 앱을 개발할 때 다음과 같은 요구 사항이 있습니다.

요건 1: 여행 계획을 위해 이것을 해주세요.
여행 제목, 여행 출발일, 어디서 몇 박, 몇 일 동안 머무를지, n일에 무엇을 할지 기록

요건 1를 만족시키기 위해 다음과 같은 영역으로 구성되어 있습니다.

/**
 * 여행 계획
 */
public class TourPlan {
    private String title; // 여행 제목
    private LocalDate startDate; // 출발 일
    private int nights; // 몇 박
    private int days; // 며칠
    private String whereToStay; // 어디서 머물지
    private List<DetailPlan> plans; // n일차 할 일
}
 
/**
 * n일차 할 일
 */
public class DetailPlan {
    private int day; // n일차
    private String plan; // 할 일
}

요건 2: 당일 여행 계획도 필요합니다.
당일치기 여행이라면 하룻밤을 묵을 필요도, 아무 데나 머물 필요도 없습니다.

필수 정보와 선택적 정보로 선택적 속성을 생성할 때 어떻게 구현할 수 있습니까?

3-1. 증분 생성자 패턴

증분 생성자 패턴생성자 오버로딩을 통해 구현할 수 있습니다.

/**
 * 기본 생성자 (필수)
 */
public TourPlan() {
}
 
/**
 * 일반적인 여행 계획 생성자
 *
 * @param title 여행 제목
 * @param startDate 출발 일
 * @param nights n박
 * @param days m일
 * @param whereToStay 머물 장소
 * @param plans n일차 할 일
 */
public TourPlan(String title, LocalDate startDate, int nights, int days,
    String whereToStay, List<DetailPlan> plans) {
    this.title = title;
    this.nights = nights;
    this.days = days;
    this.startDate = startDate;
    this.whereToStay = whereToStay;
    this.plans = plans;
}
 
/**
 * 당일치기 여행 계획 생성자
 *
 * @param title 여행 제목
 * @param startDate 출발 일
 * @param plans 1일차 할 일
 */
public TourPlan(String title, LocalDate startDate, List<DetailPlan> plans) {
    this.title = title;
    this.startDate = startDate;
    this.plans = plans;
}

위와 같이 점진적 생성자 패턴을 구현하면 선택적 인자에 따라 새로운 생성자를 생성하거나 null 값으로 채워야 하는 문제가 발생한다.

물론 롬복의 @AllArgs 생성자 주석을 사용하면 긴 코드 문제를 해결할 수 있지만 여전히 인수가 많을 때 유형 및 순서 오류 가능성이것은 존재한다

// 순서를 파악이 어렵고, 가독성이 떨어진다.
new TourPlan("여행 계획", LocalDate.of(2021,12, 24), 3, 4, "호텔",
    Collections.singletonList(new DetailPlan(1, "체크인")));
    
// 생성자를 만들지 않고 당일치기 객체를 생성하면 불필요한 Null을 채워야한다.
new TourPlan("여행 계획", LocalDate.of(2021,12, 24), null, null, null,
    Collections.singletonList(new DetailPlan(1, "놀고 돌아오기")));

사실 IntelliJ는 똑똑하고 친절해서 힌트가 톡톡 튀는 편이다.


친절한 IDE..

3-2. 자바 빈 패턴

이러한 단점을 보완하기 위해 setter 메소드를 사용하는 Java bean 패턴이 고안되었습니다.

TourPlan tourPlan = new TourPlan();
tourPlan.setTitle("칸쿤 여행");
tourPlan.setNights(2);
tourPlan.setDays(3);
tourPlan.setStartDate(LocalDate.of(2021, 12, 24));
tourPlan.setWhereToStay("리조트");
tourPlan.addPlan(1, "체크인 이후 짐풀기");
tourPlan.addPlan(1, "저녁 식사");
tourPlan.addPlan(2, "조식 부페에서 식사");
tourPlan.addPlan(2, "해변가 산책");
tourPlan.addPlan(2, "점심은 수영장 근처 음식점에서 먹기");
tourPlan.addPlan(2, "리조트 수영장에서 놀기");
tourPlan.addPlan(2, "저녁은 BBQ 식당에서 스테이크");
tourPlan.addPlan(3, "조식 부페에서 식사");
tourPlan.addPlan(3, "체크아웃");

독성이 개선되고, 주문이 자유롭기 때문에 오류 가능성이 줄어듭니다.

그러나 해결되지 않은 문제가 있습니다.

1. 함수 호출은 인수 수만큼 이루어지며 객체는 한 번에 생성할 수 없습니다.

2. 불변 객체를 생성할 수 없습니다. (세터로 값 변경 가능)
– 스레드 간에 공유할 수 있는 개체 일관성이 일시적으로 손상될 수 있습니다.

3-3. 마지막으로 빌더 패턴

Constructor 패턴과 Java Bean 패턴의 장점을 결합하여 객체 생성과 관련된 문제를 해결합니다.

  • 필요한 개체를 직접 만드는 대신 먼저 필요한 모든 인수를 생성자에 전달하여 빌더 개체를 만듭니다.
  • 그리고 선택적 인수는 읽을 수 있는 코드와 함께 전달될 수 있습니다.
  • 세터가 없기 때문에 객체의 일관성을 유지하고 불변의 객체로 생성할 수 있습니다.

기본 클래스 다이어그램은 다음과 같습니다.


출처 : 백기선 – GoF의 코딩을 통한 디자인 패턴 강의 학습

먼저 TourPlanBuilder 인터페이스를 만듭니다.

public interface TourPlanBuilder {
 
    TourPlanBuilder nightsAndDays(int nights, int days);
 
    TourPlanBuilder title(String title);
 
    TourPlanBuilder startDate(LocalDate localDate);
 
    TourPlanBuilder whereToStay(String whereToStay);
 
    TourPlanBuilder addPlan(int day, String plan);
 
    TourPlan getPlan();
 
}

이를 구현한 ConcreteBuilder는 다음과 같다.

public class DefaultTourBuilder implements TourPlanBuilder {
 
    private String title;
 
    private int nights;
 
    private int days;
 
    private LocalDate startDate;
 
    private String whereToStay;
 
    private List<DetailPlan> plans;
 
    @Override
    public TourPlanBuilder nightsAndDays(int nights, int days) {
        this.nights = nights;
        this.days = days;
        return this;
    }
 
    @Override
    public TourPlanBuilder title(String title) {
        this.title = title;
        return this;
    }
 
    @Override
    public TourPlanBuilder startDate(LocalDate startDate) {
        this.startDate = startDate;
        return this;
    }
 
    @Override
    public TourPlanBuilder whereToStay(String whereToStay) {
        this.whereToStay = whereToStay;
        return this;
    }
 
    @Override
    public TourPlanBuilder addPlan(int day, String plan) {
        if (this.plans == null) {
            this.plans = new ArrayList<>();
        }
 
        this.plans.add(new DetailPlan(day, plan));
        return this;
    }
 
    @Override
    public TourPlan getPlan() {
        return new TourPlan(title, startDate, days, nights, whereToStay, plans);
    }
}

이제 TourPlan 개체를 생성하는 코드를 살펴보겠습니다.

return tourPlanBuilder.title("칸쿤 여행")
        .nightsAndDays(2, 3)
        .startDate(LocalDate.of(2020, 12, 9))
        .whereToStay("리조트")
        .addPlan(0, "체크인하고 짐 풀기")
        .addPlan(0, "저녁 식사")
        .getPlan();

위에서 언급한 모든 문제가 해결되었습니다!

Director를 적용하면 클라이언트 코드가 더 짧아질 수 있습니다.


public class TourDirector {
 
    private TourPlanBuilder tourPlanBuilder;
 
    public TourDirector(TourPlanBuilder tourPlanBuilder) {
        this.tourPlanBuilder = tourPlanBuilder;
    }
 
    public TourPlan cancunTrip() {
        return tourPlanBuilder.title("칸쿤 여행")
                .nightsAndDays(2, 3)
                .startDate(LocalDate.of(2020, 12, 9))
                .whereToStay("리조트")
                .addPlan(0, "체크인하고 짐 풀기")
                .addPlan(0, "저녁 식사")
                .getPlan();
    }
 
    public TourPlan longBeachTrip() {
        return tourPlanBuilder.title("롱비치")
                .startDate(LocalDate.of(2021, 7, 15))
                .getPlan();
    }
}
public static void main(String() args) {
    TourDirector director = new TourDirector(new DefaultTourBuilder());
    TourPlan tourPlan = director.cancunTrip();
}

다음은 빌더 패턴의 몇 가지 장점입니다.

필요한 데이터만 설정 가능
유연성 확보 가능
가독성을 높일 수 있습니다
불변성 확보 가능

빌더 패턴은 가장 많이 사용되는 생성 패턴 중 하나로 Retrofit, Okhttp 등 유명 오픈소스도 이 빌더 패턴을 사용한다.
실제로 Stream.Builder API, StringBuilder 및 UriComponentsBuilder가 사용됩니다.

위에서 언급한 장점을 이해하고 필요하다면 원장을 잘 활용하자.