[객체 생성 패턴] Chapter 4-2. Builder Pattern : 패턴 적용하기

 

✍️ 빌더 패턴, 적용하기

빌더 패턴을 적용하려면 빌더 인터페이스를 만들어야 한다.

 

빌더 인터페이스는 일관된 프로세스로 인스턴스를 구성하고 생성할 수 있도록 추상화된 메서드를 제공해야 한다.

public interface TourPlanBuilder {
    TourPlanBuilder title(String title);

    TourPlanBuilder nightsAndDays(int nights, int days);

    TourPlanBuilder startDate(LocalDate startDate);

    TourPlanBuilder whereToStay(String whereToStay);

    TourPlanBuilder addPlan(int day, String plan);

    TourPlan getPlan();
}

 

추상 메서드들을 살펴보면 공통적인 특징으로 TourPlanBuilder를 반환한다. 멤버 변수 초기화 메서드들이 TourPlanBuilder를 반환하는 몇 가지 이유 중 하나가 메서드 체이닝을 사용하기 위해서다.

 

가령 TourPlanBuilder의 구현 클래스를 사용하는 클라이언트는 title 메서드를 호출하고 나면 TourPlanBuilder 타입의 인스턴스를 반환받아 TourPlanBuilder의 또 다른 메서드를 호출할 수 있다. 클라이언트는 일련의 체이닝 과정을 거치며 인스턴스를 구성하고, 구성 작업이 끝나면 getPlan 메서드를 호출해 실제 인스턴스를 반환받는다.

 

구현 클래스, DefaultTourPlanBuilder 

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 title(String title) {
        this.title = title;

        return this;
    }

    @Override
    public TourPlanBuilder nightsAndDays(int nights, int days) {
        this.nights = nights;
        this.days = days;

        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) {
            plans = new ArrayList<>();
        }

        this.plans.add(new DetailPlan(day, plan));

        return this;
    }

    @Override
    public TourPlan getPlan() {
        return new TourPlan(title, nights, days, startDate, whereToStay, plans);
    }
}

본 코드에선 생략됐지만 getPlan 메서드에선 인스턴스가 올바른 값으로 초기화되었는지, 불안정한 상태는 아닌지에 검증하는 단계가 포함되어야 한다. 가령 초기화되지 않은 필드가 있는지 또는 장거리 여행에서 nights 와 days가 초기화되었는지와 같은 검증을 거쳐 인스턴스를 생성해야 한다.

 

🍊 빌더 패턴, 사용하기

클라이언트는 DefaultTourBuilder의 메서드를 체이닝 형태로 호출하며 인스턴스를 구성하고 최종적으로 Builder가 생성한 인스턴스를 반환받는다. 

public class App {
    public static void main(String[] args) {
        TourPlanBuilder builder1 = new DefaultTourBuilder();
        TourPlan jeJuPlan = builder1.title("제주도 여행")
                .nightsAndDays(2, 3)
                .startDate(LocalDate.of(2020, 12, 9))
                .whereToStay("리조트").addPlan(0, "체크인")
                .addPlan(0, "한라산 등반")
                .addPlan(1, "한라봉 먹기")
                .addPlan(2, "흑돼지 먹기")
                .addPlan(3, "귀가")
                .getPlan();

        TourPlanBuilder builder2 = new DefaultTourBuilder();
        TourPlan busanPlan = builder2.title("[당일치기] 부산 여행")
                .startDate(LocalDate.of(2022, 5, 15))
                .addPlan(0, "바닷가에서 놀기")
                .getPlan();
    }
}

 

만약 빌더를 사용하지 않고 생성자를 사용했다면

생성자의 매개변수 값들을 장황하게 작성했을 것이다.

하물며 초기화하지 않는 값들엔 null 값을 넘겨주면서 난잡한 코드가 됐을 것이다. 

 

🥝 빌더 패턴, 디렉터 추가

빌더를 통한 인스턴스 생성이 반복적으로 이뤄진다면 디렉터 사용을 고려해 볼 필요가 있다.

아래 코드를 예로 들어, jejuTripPlan을 생성하는 코드가 반복적으로 등장한다면 디렉터의 메서드에 jejuTripPlan 생성 코드를 넣어두고 클라이언트는 이를 호출함으로써 코드를 재사용할 수 있다.  

public class App {
    public static void main(String[] args) {
        TourPlanBuilder builder1 = new DefaultTourBuilder();
        TourPlan jejuTripPlan = builder1.title("제주도 여행")
                .nightsAndDays(2, 3)
                .startDate(LocalDate.of(2020, 12, 9))
                .whereToStay("리조트").addPlan(0, "체크인")
                .addPlan(0, "한라산 등반")
                .addPlan(1, "한라봉 먹기")
                .addPlan(2, "흑돼지 먹기")
                .addPlan(3, "귀가")
                .getPlan();

        TourPlanBuilder builder2 = new DefaultTourBuilder();
        TourPlan busanTripPlan = builder2.title("[당일치기] 부산 여행")
                .startDate(LocalDate.of(2022, 5, 15))
                .addPlan(0, "바닷가에서 놀기")
                .getPlan();
    }
}

 

디렉터, TourDirector

public class TourDirector {
    private TourPlanBuilder builder;

    public TourDirector(TourPlanBuilder builder) {
        this.builder = builder;
    }

    public TourPlan jejuTripPlan() {
        return builder.title("제주도 여행")
                .nightsAndDays(2, 3)
                .startDate(LocalDate.of(2020, 12, 9))
                .whereToStay("리조트").addPlan(0, "체크인")
                .addPlan(0, "한라산 등반")
                .addPlan(1, "한라봉 먹기")
                .addPlan(2, "흑돼지 먹기")
                .addPlan(3, "귀가")
                .getPlan();
    }

    public TourPlan busanTripPlan() {
        return builder.title("[당일치기] 부산 여행")
                .startDate(LocalDate.of(2022, 5, 15))
                .addPlan(0, "바닷가에서 놀기")
                .getPlan();
    }
}

 

디렉터는 직접 빌더의 메서드를 호출하며 일관된 프로세스로 인스턴스를 만들고
클라이언트 쪽에선 디렉터가 제공하는 메서드를 호출하므로써 코드를 재사용할 수 있다.

public class App {
    public static void main(String[] args) {
        TourDirector director = new TourDirector(new DefaultTourBuilder());
        TourPlan jejuTripPlan = director.jejuTripPlan();
        TourPlan busanTripPlan = director.busanTripPlan();
    }
}

 

 

인프런의 백기선님의 강의 코딩으로 학습하는 GoF의 디자인 패턴을 참고해서 작성했습니다.

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com