Java/디자인 패턴

[디자인 패턴] 빌더 패턴

태감새 2023. 4. 19. 15:09

코딩으로 학습하는 GoF의 디자인 패턴

빌더

동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법

Director는 추가적인 기능이고 실질적인 빌더 패턴은 Builder 인터페이스와 그 구현체가 전부이다.

예제

TourPlanBuilder

  • 빌더의 인터페이스. 각 메서드는 필드의 이름을 메서드 이름으로 지정하고 같은 타입을 매개변수로 받는다.
  • 반환 타입은 TourBuilderInterface로 해서 체이닝이 가능하도록 한다.
  • 마지막 build()는 마무리로 실제 객체를 반환해야 하므로 반환 타입을 TourPlan으로 한다.
public interface TourPlanBuilder {  
   TourPlanBuilder nightsAndDays(int nights, int days);  

   TourPlanBuilder title(String title);  

   TourPlanBuilder startDate(LocalDate startDate);  

   TourPlanBuilder whereToStay(String whereToStay);  

   TourPlanBuilder addPlan(int day, String plan);  

   TourPlan build();  
}

DefaultTourBuilder

  • TourPlanBuilder의 구현체
    • 빌더를 구성하는 방식은 두가지가 존재한다.
    • 필드에 생성할 객체를 입력해서 set으로 필드값 입력하기 (현재방식)
    • 생성할 객체의 모든 필드를 빌더 필드에 입력 후 build()시 생성자를 통해서 만들기
  • 오버라이드한 메서드들은 각자의 필드에 주입시켜주고 객체 자신을 반환한다. (this)
  • build()는 필요한 객체를 반환하도록 만든다.
public class DefaultTourBuilder implements TourPlanBuilder{  

   private TourPlan tourPlan;  

   public DefaultTourBuilder() {  
      this.tourPlan = new TourPlan();  
   }  

   public static TourPlanBuilder builder() {  
      return new DefaultTourBuilder();  
   }  

   @Override  
   public TourPlanBuilder nightsAndDays(int nights, int days) {  
      tourPlan.setNights(nights);  
      tourPlan.setDays(days);  
      return this;  
   }  

   @Override  
   public TourPlanBuilder title(String title) {  
      tourPlan.setTitle(title);  
      return this;  
   }  

   @Override  
   public TourPlanBuilder startDate(LocalDate startDate) {  
      tourPlan.setStartDate(startDate);  
      return this;  
   }  

   @Override  
   public TourPlanBuilder whereToStay(String whereToStay) {  
      tourPlan.setWhereToStay(whereToStay);  
      return this;  
   }  

   @Override  
   public TourPlanBuilder addPlan(int day, String plan) {  
      if(tourPlan.getPlans() == null)  
         tourPlan.setPlans(new ArrayList<>());  
      tourPlan.getPlans().add(new DetailPlan(day, plan));  
      return this;  
   }  

   @Override  
   public TourPlan build() {  
      return tourPlan;  
   }  

}

TourDirector

  • 객체를 한 번더 감싸는 방식
  • 일반적으로 자주 사용하는 패턴이 있다면 Director의 메서드로 지정하는 것도 좋은 방법이다.
public class TourDirector {  

   public TourPlan tour1() {  
      return DefaultTourBuilder.builder()  
         .title("칸쿤 여행")  
         .nightsAndDays(2,3)  
         .startDate(LocalDate.of(2020,12,9))  
         .whereToStay("리조트")  
         .addPlan(0, "체크인하고 짐 풀기")  
         .addPlan(0, "저녁 식사")  
         .build();  
   }  

   public TourPlan tour2() {  
      return DefaultTourBuilder.builder()  
         .title("롱비치")  
         .startDate(LocalDate.of(2021, 7, 25))  
         .build();  
   }  
}

App

  • 사용 예시
public class App {  

   public static void main(String[] args) {  
      // 빌더 사용하기
      TourPlan tour1 = DefaultTourBuilder.builder()  
         .title("칸쿤 여행")  
         .nightsAndDays(2,3)  
         .startDate(LocalDate.of(2020,12,9))  
         .whereToStay("리조트")  
         .addPlan(0, "체크인하고 짐 풀기")  
         .addPlan(0, "저녁 식사")  
         .build();  

      TourPlan tour2 = DefaultTourBuilder.builder()  
         .title("롱비치")  
         .startDate(LocalDate.of(2021, 7, 25))  
         .build();  

      // Director 사용하기  
      TourDirector director = new TourDirector();  
      TourPlan directedTour1 = director.tour1();  
      TourPlan directedTour2 = director.tour2();  
   }  
}

장/단점

장점

  1. 복잡한 객체를 순차적으로 만들 수 있다. (설계를 잘하면)
  2. 객체를 만드는 과정을 숨길 수 있다.
  3. 동일한 프로세스를 통해 각기 다른 객체를 만들 수 있다.
  4. 불완전한 객체를 사용하지 못하도록 방지할 수 있다.

단점

  1. 원하는 객체를 만들려면 빌더 객체먼저 만들어야 한다.
  2. 구조가 복잡해진다.

개인적인 의견

+가독성

개인적으로 생성자가 아닌 빌더 패턴을 이용해서 객체를 생성하는 경우 가독성이 좋아진다고 생각한다. 필드명을 메서드로 직접적으로 명시해주니까 실수하는 경우도 줄어든다. 생성자를 사용해서 객체를 만드는 방식보다 훨씬 직관적이다.

+활용성

생성자를 통한 객체 생성에서는 입력되는 매개변수가 다른 생성자를 사용하려면 새로운 생성자를 만들어줘야 한다. 경우의 수가 생겨날수록 만들어야하는 생성자의 수도 증가한다. 빌더패턴은 모든 매개변수를 가지는 생성자를 만들어놓고 빌드할 때 입력되지 않은 필드의 값은 null처리 또는 기본값처리를 해줄 수 있다.
생성자로 객체를 만드는 방식도 가능은 하지만 생성자 매개변수의 순서를 항상 알고있어야 처리가 가능하다. 빌더 객체는 순서가 상관없기 때문에 필요한 필드값만 입력하고 빌드하면 알아서 처리가 되므로 더 편하게 다양한 조건의 객체를 생성할 수 있다.

-복잡성

빌더는 메서드를 체이닝하는 방식으로 설정을 이어나가는데 단순 객체 생성뿐만 아니라 여러가지 활용이 가능하다. 함수형 인터페이스를 입력받아서 람다식을 사용할 수도 있고 빌더 안에 다른 빌더를 넣을수도 있고 여러 빌더를 상속받아서 거대한 빌더를 형성할 수도 있다. 이게 이해하면 직관적으로 다가오지만 처음 접하면 상당히 복잡하게 느껴진다. 그래서 크기가 거대해지면 복잡해진다는 단점이 있지만 이해에 대한 복잡성이지 코드는 깔끔하게 작성된다. 그래서 빌더를 활용하기 위해서는 어느정도 빌더의 이해가 필요하다고 생각된다.