개발자로 후회없는 삶 살기

디자인 패턴 PART.빌더 패턴 본문

[백엔드]/[Java | 학습기록]

디자인 패턴 PART.빌더 패턴

몽이장쥰 2023. 8. 17. 15:13

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard 

 

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

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

www.inflearn.com

 

 

본론

- 빌더 패턴 소개

어떤 객체를 만들 때 객체가 다양한 구성으로 만들어질 수 있는데 동일한 프로세스를 통해서 만들 수 있게 해주는 패턴입니다.

 

=> 코드

public class App {
    
    public static void main(String[] args) {      
        TourPlan tourPlan = new TourPlan();
        tourPlan.setTitle("칸쿤 여행");
        tourPlan.setNights(2);
        tourPlan.setDays(3);
        tourPlan.setStartDate(LocalDate.of(2020, 12, 9));
        tourPlan.setWhereToStay("리조트");
        tourPlan.addPlan(0, "체크인 이후 짐풀기");
        tourPlan.addPlan(0, "저녁 식사");
        tourPlan.addPlan(1, "조식 부페에서 식사");
        tourPlan.addPlan(1, "해변가 산책");
        tourPlan.addPlan(1, "점심은 수영장 근처 음식점에서 먹기");
        tourPlan.addPlan(1, "리조트 수영장에서 놀기");
        tourPlan.addPlan(1, "저녁은 BBQ 식당에서 스테이크");
        tourPlan.addPlan(2, "조식 부페에서 식사");
        tourPlan.addPlan(2, "체크아웃");
    }
}

여행 상품 TourPlan 객체를 만드는 클라이언트 코드입니다.

 

public class TourPlan {
    
    private String title;
    
    private int nights;
    
    private int days;
    
    private LocalDate startDate;
    
    private String whereToStay;
    
    private List<DetailPlan> plans;

TourPlan 코드는 이름, 몇 박, 시작 날, 구체적인 계획들을 필드로 가지고 있으며

 

public class DetailPlan {
    
    private int day;
    
    private String plan;

구체적인 계획에 해당하는 객체를 가지고 있을 때 간단하게 칸쿤으로 가는 2박 3일 여행 계획을 만들 수 있습니다.

 

-> 짧은 여행이라면? ✅

TourPlan shortTrip = new TourPlan();
shortTrip.setTitle("오레곤 롱비치 여행");
shortTrip.setStartDate(LocalDate.of(2021, 7, 15));

shortTrip을 만들고 여행 이름, 시작날짜 등 짧지만 역시 계획을 만들 수 있습니다.

 

-> 설명

하지만 이 둘을 보면 계획을 짜는데 장황하고 일관된 프로세스가 없다는 것을 알 수 있고 박을 쓰면 일을 강제해야하는 것도 못 해서 객체가 불안정한 모습입니다.

 

public TourPlan() {
}

public TourPlan(String title, int nights, int days, LocalDate startDate, String whereToStay, List<DetailPlan> plans) {
    this.title = title;
    this.nights = nights;
    this.days = days;
    this.startDate = startDate;
    this.whereToStay = whereToStay;
    this.plans = plans;
}

public TourPlan(String title) {
    this.title = title;
}

또한 생성자를 만들 때 짧은 여행이면 title만 있으면 되고 긴 여행이면 장소도 있어햐 하는 등 생성자도 다 다른 모양으로 많아지게 됩니다.

 

이때 빌더 패턴을 적용하면 빌더에 객체를 만드는 방법을 인터페이스 안에 스탭별(A, B)로 만들어서 최종적으로 객체를 만들 수 있는 getProduct라는 메서드를 만들어서 구현체를 만들도록 합니다.

 

이렇게 구현체와 인터페이스의 관계라서 우리가 원한다면 인스턴스를 만드는 방법을 또 다른 구체 빌더를 만들어서 다양하게 객체를 만드는 빌더를 구성할 수 있습니다.

 

클라이언트가 구체 빌더를 사용해도 되지만 디렉터를 둬서 클라이언트가 디렉터를 사용하고 디렉터가 빌더를 사용하도록 하면 디렉터 안에 반복되는 빌더 호출 스택을 숨겨 놓을 수 있고 클라이언트는 디렉터를 통해서 빌더를 간단하게 사용할 수 있습니다.

 

- 패턴 적용하기

이전 코드를 빌더 패턴으로 바꿔보도록 하겠습니다.

 

1. 빌더 인터페이스

인터페이스에 어떠한 단계를 거쳐서 최종적인 tourPlan이라는 객체를 만들지를 정의합니다.

 

public interface TourPlanBuilder{
    TourPlanBuilder title(String title);
}

TourPlan 빌더 타입을 반환하는 메서드를 먼저 만드는데 이를 통해서 메서드 체이닝을 할 수 있습니다. 예를들어 클라이언트에서 title을 호출하게 되면 TourPlan 빌더를 받게 되어서 TourPlan 빌더의 메서드를 사용할 수 있습니다.

 

public interface TourPlanBuilder{
    TourPlanBuilder title(String title);
    
    TourPlanBuilder nightsAndDays(int nights, int days);
    
    TourPlan getPlan();
}

title을 쓴 후 nightsAndDays를 바로 연달아서 쓸 수 있습니다. 마지막으로는 TourPlan getPlan()을 호출하면 최종적으로 객체를 리턴 받을 수 있습니다.

 

- 구현 빌더

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 TourPlan getPlan() {
    return new TourPlan(title, nights, days, startDate, whereToStay, plans);
}

최종적으로 TourPlan을 리턴하도록 할 것입니다. 

 

- 클라이언트

public class App {
    public static void main(String[] args) {
        TourPlanBuilder builder = new DefaultTourBuilder();
        TourPlan plan = builder.title("칸쿤 여행")
                .nightsAndDays(2, 3)
                .startDate(LocalDate.of(2022, 12, 3))
                .whereToStay("리조트")
                .addPlan(0, "체크인")
                .addPlan(0, "바베큐")
                .getPlan();
    }
}

그러면 이제 클라이언트가 바로 사용할 수 있습니다. 빌더 인터페이스를 선언하고 구현 빌더를 생성하고 빌더의 메서드를 원하는 대로 체이닝한 후 get하면 TourPlan이 생성됩니다.

 

builder.title("롱비치")
        .startDate(LocalDate.of(2021, 7, 15))
        .getPlan();

shortTrip은 원하는 메서드만 선택해서 만들 수 있습니다. 우리가 이걸 생성자로 만들었으면 생성자가 장황해지고 생성자의 파라미터로 수많은 null 값을 넣어야 했을 것입니다.

 

-> 디렉터

이렇게 객체를 만드는 프로세스가 자주 반복이 된다면 이런 세트를 디렉터에 만들어 놓고 재사용할 수도 있습니다.

 

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(2022, 12, 3))
                .whereToStay("리조트")
                .addPlan(0, "체크인")
                .addPlan(0, "바베큐")
                .getPlan();
    }
}

TourDirector 클래스를 만들고 생성자에서 빌더를 주입받은 후 cancunTrip을 아예 세트로 메서드로 만들어 버립니다.

 

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

이렇게 하면 클라이언트는 빌더를 직접 건드리지 않고 정형화된 디렉터를 통해서 여행 계획 객체를 만들 수 있습니다.

 

- 장, 단점

1. 장점

1) 만들기 복잡한 객체를 순차적으로 만들 수 있습니다. 객체를 만드는 것을 규격화 하는 것입니다. 객체 내부에 생성자를 장황하게 만들 필요가 없어집니다.

2) 디렉터를 통해서 만드는 과정을 숨길 수 있습니다.

3) 동일한 프로세스를 통해서 다른 객체를 만들 수 있습니다. VIP 투어 빌더를 기획하고 동일한 프로세스를 거치지만 다른 객체가 나오게 할 수 있습니다.

2. 단점

객체를 만드는데 반드시 빌더나 디렉터를 만들어야 합니다.

Comments