제품을 여러 단계로 나눠서 만들 수 있도록 제품 생산 단계들을 캡슐화하고 싶다면 빌더패턴을 사용하면 됩니다.
아래는 여행계획을 기록할 인스턴스들 입니다.
public class TourPlan {
private String title;
private int nights;
private int days;
private LocalDate startDate;
private String whereToStay;
private List<DetailPlan> plans;
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;
}
@Override
public String toString() {
return "TourPlan{" +
"title='" + title + '\'' +
", nights=" + nights +
", days=" + days +
", startDate=" + startDate +
", whereToStay='" + whereToStay + '\'' +
", plans=" + plans +
'}';
}
// getter, setter ....
public void addPlan(int day, String plan) {
this.plans.add(new DetailPlan(day, plan));
}
}
public class DetailPlan {
private int day;
private String plan;
public DetailPlan(int day, String plan) {
this.day = day;
this.plan = plan;
}
// getter, setter ....
@Override
public String toString() {
return "DetailPlan{" +
"day=" + day +
", plan='" + plan + '\'' +
'}';
}
}
public class App {
public static void main(String[] args) {
TourPlan shortTrip = new TourPlan();
shortTrip.setTitle("오레곤 롱비치 여행");
shortTrip.setStartDate(LocalDate.of(2021, 7, 15));
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, DetailPlan의 인스턴스를 생성하여 아래와 같이 여행 계획을 세우는 코드가 있습니다.
보시면 상당히 장황하다는 것을 알 수 있습니다. 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됩니다. 이렇게 되면 반드시 설정해야 하는 값들이 실수로 누락되는 경우가 생길 수 있고 강제할 수 있는 방법이 없고 불변으로 만들 수 없습니다. 이런 경우에 빌더패턴을 사용하면 빌더에다가 인스턴스를 생성하는 방법을 step별로 만들어 좀 더 깔끔하게 . stapA, stepB 이런식으로 인스턴스에다가 정의하고 최종적으로 어떤 인스턴스를 받아올 수 있는 getProduct라는 인터페이스를 클래스 내부에 만들어 놓고 구현체를 생성합니다. 따라서 구현체와 인터페이스의 관계기 때문에 우리가 원한다면 인스턴스를 만드는 방법을 또 다른 구체적인 빌더를 만듦으로서 다양하게 객체들을 만들 수 있는 빌더들을 구성할 수 있습니다.
위에 장황한 코드를 빌더 패턴을 적용해서 코드를 수정해 보겠습니다. 빌더 패턴을 적용하기위해서 먼저 빌더 인터페이스에 어떠한 과정을 주어서 최종적인 TourPlan인스턴스를 생성할지 정의합니다.
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();
}
리턴타입들이 TourPlanBuilder인 이유는 인터페이스를 사용하는 클라이언트쪽 코드에서 nightsAndDays라는 메서드를 호출하고 나면 TourPlanBuilder타입의 인스턴스를 받게 됩니다. 그럼 이 안에서 제공하는 또다른 title메서드를 호출할 수 있고 title메서드도 마찬가지로 TourPlanBuilder타입의 인스턴스를 받게되기 때문에 체이닝이 가능해 집니다. getPlan메서드를 호출해서 TourPlan을 호출할때 까지 체이닝으로 인스턴스의 값들을 설정할 수 있습니다. 이렇게 되면 getPlan메서드에서 체이닝으로 설정한 값들을 검증할 수도 있습니다. 이제 구현체를 만들어 보겠습니다.
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, nights, days, startDate, whereToStay, plans);
}
}
TourPlanBuilder를 구현하여 각 메서드의 값들을 세팅하고 this를 통해서 DefaultTourBuilder인스턴스를 리턴하도록 합니다. 그리고 getPlan메서드를 통해서 이전에 체이닝으로 설정한 값들을 생성자를 통해서 TourPlan인스턴스를 리턴하도록 구현하였습니다. 이제 구현한 빌더를 클라이언트에서 사용해 보도록 하겠습니다.
public class App {
public static void main(String[] args) {
TourPlanBuilder builder = new DefaultTourBuilder();
TourPlan plan = builder.title("칸쿤 여행")
.nightsAndDays(2, 3)
.startDate(LocalDate.of(2020, 12, 9))
.whereToStay("리조트")
.addPlan(0, "체크인하고 짐 풀기")
.addPlan(0, "저녁 식사")
.getPlan();
TourPlan longBeachTrip = builder.title("롱비치")
.startDate(LocalDate.of(2020, 12, 9))
.getPlan();
}
}
DefaultTourBuilder의 인스턴스를 생성하고 이 인스턴스를 통해서 체이닝으로 각 TourPlan인스턴스를 생성했습니다. 이 부분을 생성자로 구현하면 설정값들이 많기 때문에 생성자의 매개변수가 장황해 지고 필요없는 값들은 null값으로 설정해야 합니다. 또한 실수로 같은타입의 매개변수의 순서를 바꿔서 설정하는 경우 컴파일타임에서 잡히지 않기 때문에 나중에 런타임환경에서 찾기 상당히 어려워 집니다.
만약 TourPlan인스턴스를 생성하는일이 자주 반복이 된다고 하면 미리 만들어져 있는 인스턴스를 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();
}
}
TourPlanBuilder를 주입받아서 미리 TourPlan인스턴스를 생성하는 로직을 구현한다. 이 코드를 클라이언트에서 사용해 보겠습니다.
public class App {
public static void main(String[] args) {
TourDirector director = new TourDirector(new DefaultTourBuilder());
TourPlan tourPlan = director.cancunTrip();
TourPlan tourPlan1 = director.longBeachTrip();
}
}
클라이언트코드에서 할 일은 직접 TourPlan의 인스턴스를 만드는게 아니라 칸쿤 여행과 롱비치 여행의 인스턴스가 미리 선언된 TourDirector인스턴스를 통해서 cancunTrip, longBeachTrip메서드로 호출만 하면 됩니다. 클라이언트의 코드가 아주 간단해 졌습니다.
빌더패턴의 장점은 만들기 복잡한 객체를 순차적으로 만들 수 있는 방법을 제공해 줄 수 있습니다.
예를 들어 만드는 순서가 정해진 인스턴스 같은 경우는 순서를 강제할 수도 있습니다. 그리고 빌더의 인스턴스가 복잡하지만 그 순서를 따라가다 보면은 생성자에 매개변수를 주입하는 방법보다 훨씬 오류를 발견하기 쉬워집니다. 또한 복잡한 객체를 만드는 과정을 Director를 통해 숨길 수 있습니다.
Director나 Builder인스턴스를 먼저 생성해야 인스턴스를 생성할 수 있고 인스턴스를 생성하는 로직이 Director와 Builder를 거쳐서 생성하기 때문에 좀 더 로직이 복잡해진다는 단점이 있습니다.