Spring - Builder 패턴이 뭐야?

조예슬·2023년 5월 8일
1

Server

목록 보기
6/6
post-thumbnail

☑️ 빌더 패턴이 뭐야 ?

  • 빌더 패턴은 복잡한 객체를 생성하는 방법을 정의하는 클래스표현하는 방법을 정의하는 클래스별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴이다.
  • 생성해야하는 객체가 Optional한 속성을 많이 가질 때가 더 좋다.
  • 빌더 패턴은 생성 패턴 중 하나이다.
    • 생성 패턴은 인스턴스를 만드는 절차를 추상화하는 패턴이다.
    • 생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리해준다.
    • 생성 패턴은 다음 두 가지 특징이 있다.
      • 시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화한다.
      • 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려준다.
    • 다시 말해, 생성 패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며 어떻게 생성하는지 결정하는데 유연성을 확보할 수 있게 된다.

☑️ 빌더 패턴을 왜 쓰는건데 ?

  • 빌더 패턴은 객체를 생성할 때 생성자만 사용할 때 발생할 수 있는 문제를 개선하기 위해 사용된다.
  • 생성 패턴 말고도 팩토리 메소드 패턴이나 추상 팩토리 패턴에서는 생성해야하는 클래스에 대한 속성 값이 많을 때 아래와 같은 이슈가 발생한다.
    • 클라이언트가 팩토리 클래스를 호출할 때 파라미터로 넘겨주는 값의 타입, 순서 등에 대한 관리가 어려워져 에러가 발생할 확률이 높아진다.
    • 경우에 따라 필요 없는 파라미터들에 대해서는 팩토리 클래스에 일일이 Null 값을 넘겨줘야 한다.
    • 생성해야 하는 sub class가 무거워지고 복잡해짐에 따라 팩토리 클래스 역시 복잡해진다.
  • 빌더 패턴은 이런 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값들에 대해서는 메소드를 통해 step-by-step으로 값을 입력받은 후에 build() 메소드를 통해 최종적으로 하나의 인스턴스를 return하는 방식이다.

☑️ 그럼 이 빌더 패턴 어떻게 써 ?

  • 요구사항을 기반으로 여행 계획을 세우는 앱을 개발한다고 할 때, 다음과 같은 요구사항이 있다.
  1. 요구사항1 : 여행 계획 항목은 이렇게 해주세요.
    1. 여행 제목, 여행 장소, 여행 출발일, 몇박 몇일동안 어디서 머물지, n일차에 하루 계획을 기록
  • 위의 요구사항1을 만족시키는 도메인은 다음과 같이 구성될 것이다.
/**
 * 여행 계획
 */
public class TourPlan {
    private String title; // 여행 제목
		private String place; // 여행 장소
    private LocalDate startDate; // 출발일
    private int nights; // n박
    private int days; // m일
    private List<DailyPlan> plans; // m일차 하루 계획
}
 
/**
 * n일차 하루 계획
 */
public class DailyPlan {
    private int day; // n일차
		private String place; // 갈 곳
    private String doing; // 할 일
}
  1. 요구사항2 : 여행은 꼭 n박 m일이 아니고, 당일 치기일 수도 있어요 !
    1. 당일 치기는 n박 m일이 필요 없고, 어디서 머물지도 필요없다.
  • 위와 같이, 필수적인 정보와 선택적인 정보로 Optional한 속성들이 생겼을 때 어떻게 구현하면 될까 ?

1) 점층적 생성자 패턴

  • 점층적 생성자 패턴을 적용하면 생성자 오버로딩을 통해 구현 가능하다.
/**
 * 기본 생성자 (필수)
 */
public TourPlan() {
}
 
/**
 * 일반적인 여행 계획 생성자
 *
 * @param title 여행 제목
 * @param startDate 출발 일
 * @param nights n박
 * @param days m일
 * @param whereToStay 머물 장소
 * @param plans n일차 할 일
 */
public TourPlan(String title, String place, LocalDate startDate, int nights, int days,
    List<DailyPlan> plans) {
    this.title = title;
		this.place = place;
    this.nights = nights;
    this.days = days;
    this.startDate = startDate;
    this.plans = plans;
}
 
/**
 * 당일치기 여행 계획 생성자
 *
 * @param title 여행 제목
 * @param startDate 출발 일
 * @param plans 1일차 할 일
 */
public TourPlan(String title, String place, LocalDate startDate, List<DailyPlan> plans) {
    this.title = title;
		this.place = place;
    this.startDate = startDate;
    this.plans = plans;
}
  • 위와 같이 점층적 생성자 패턴으로 구현하면, Optional한 인자에 따라 새로운 생성자를 만들거나, Null 값으로 채워야하는 문제가 있다.
  • 뭐 Lombok의 @AllArgsConstructor 어노테이션을 사용하면 코드가 길어지는 문제는 해결 가능하지만, 생성자에 이렇게 인자가 많으면 타입과 순서로 발생할 수 있는 에러 가능성이 있다.
// 순서를 파악이 어렵고, 가독성이 떨어진다.
new TourPlan("여봉봉과 함께하는 1주년 여행", "부산", LocalDate.of(2021,12, 24), 3, 4,
    Collections.singletonList(new DailyPlan(1, "자갈치 시장", "회떠오기")));
    
// 생성자를 만들지 않고 당일치기 객체를 생성하면 불필요한 Null을 채워야한다.
new TourPlan("전주 맛집 투어", "전주", LocalDate.of(2021,12, 24), null, null, null,
    Collections.singletonList(new DetailPlan(1, "한옥마을", "한복 입기")));

2) 자바 빈(Bean) 패턴

  • 이러한 단점을 보완하기 위해 setter 메소드를 사용한 자바 빈 패턴이 생겼다.
TourPlan tourPlan = new TourPlan();
tourPlan.setTitle("칸쿤 여행");
tourPlan.setNights(2);
tourPlan.setDays(3);
tourPlan.setStartDate(LocalDate.of(2021, 12, 24));
tourPlan.setPlace("칸쿤");
tourPlan.addPlan(1, "어쩌고 호텔", "체크인 이후 짐풀기");
tourPlan.addPlan(1, "어쩌고 호텔","저녁 식사");
tourPlan.addPlan(2, "어쩌고 호텔", "조식 먹기");
tourPlan.addPlan(2, "저쩌고 비치", "해변가 산책");
tourPlan.addPlan(2, "이러쿵 음식점", "점심은 수영장 근처 음식점에서 먹기");
...
tourPlan.addPlan(3, "어쩌고 호텔", "체크아웃");
  • 가독성도 좋아지고 순서의 제약에서도 어느정도 벗어나기 때문에 에러 발생 가능성도 줄어든다.
  • 하지만 과연 문제가 없을까 ?
    • 함수 호출이 인자만큼 이루어지고, 객체 호출을 한번에 할 수 없다.
    • 불변(immutable) 객체를 생성할 수 없다. (setter로 값이 변경 가능하기 대문이다)
      • 쓰레드간 공유 가능한 객체 일관성(consistency)이 일시적으로 깨질 수 있다.

3) 빌더 패턴

  • 생성자 패턴과 자바 빈 패턴의 장점을 결합하여 객체 생성과 관련된 문제를 해결한다.
  • 필요한 객체를 직접 생성하지 않고, 먼저 필수 인자들을 생성자에 전부 전달해서 빌더 객체를 만든다.
  • 그리고 선택 인자는 가독성이 좋은 코드로 인자를 넘길 수 있다.
  • setter가 없으므로 객체 일관성을 유지하여 불변 객체로 생성 가능하다.

백기선님 - 코딩으로 학습하는 GoF의 디자인 패턴 강의 자료

  • 인터페이스인 TourPlanBuilder를 만들어준다.
public interface TourPlanBuilder {
 
    TourPlanBuilder nightsAndDays(int nights, int days);
 
    TourPlanBuilder title(String title);
 
    TourPlanBuilder startDate(LocalDate localDate);
 
    TourPlanBuilder place(String place);
 
    TourPlanBuilder addPlan(int day, String place, String doing);
 
    TourPlan getPlan();
 
}
  • 이를 구현하는 구현체 ConcreteBuilder를 만들어준다.
public class DefaultTourBuilder implements TourPlanBuilder {
 
    private String title;
 
    private int nights;
 
    private int days;
 
    private LocalDate startDate;
 
    private String place;
 
    private List<DeilyPlan> 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 place) {
        this.place = place;
        return this;
    }
 
    @Override
    public TourPlanBuilder addPlan(int day, String place, String doing) {
        if (this.plans == null) {
            this.plans = new ArrayList<>();
        }
 
        this.plans.add(new DailyPlan(day, place, doing));
        return this;
    }
 
    @Override
    public TourPlan getPlan() {
        return new TourPlan(title, startDate, days, nights, place, plans);
    }
}
  • 이렇게 하면 다음과 같이 TourPlan 객체를 생성할 수 있다.
return tourPlanBuilder.title("칸쿤 여행")
        .nightsAndDays(2, 3)
        .startDate(LocalDate.of(2020, 12, 9))
        .place("칸쿤")
        .addPlan(1, "어쩌고 호텔", "체크인하고 짐 풀기")
        .addPlan(1, "어쩌고 호텔", "저녁 식사")
        .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))
				        .place("칸쿤")
				        .addPlan(1, "어쩌고 호텔", "체크인하고 짐 풀기")
				        .addPlan(1, "어쩌고 호텔", "저녁 식사")
				        .getPlan();
    }
 
    public TourPlan busanTrip() {
        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();

☑️ 근데 이 빌더 패턴을 Spring에 어떻게 접목한다는거 ?

  • 그래서 Java 개발자들의 보일러플레이트 코드를 기똥차게 줄여준 라이브러리인 Lombok을 사용하는 것이다 !
		@AllArgsConstructor(access = AccessLevel.PRIVATE)
    @Builder(builderMethodName = "**travelCheckListBuilder**")
    @ToString
    public class TravelCheckList {

        private Long id;
        private String passport;
        private String flightTicket;
        private String creditCard;
        private String internationalDriverLicense;
        private String travelerInsurance;

        public static TravelCheckListBuilder **builder**(Long id) {
            if(id == null) {
                throw new IllegalArgumentException("필수 파라미터 누락");
            }
            return **travelCheckListBuilder**().id(id);
        }
    }
  • @AllArgsConstructor(access = AccessLevel.PRIVATE)
    • @Builder 애노테이션을 선언하면 전체 인자를 갖는 생성자를 자동으로 만든다.
    • @AllArgsConstructor는 전체 인자를 갖는 생성자를 만드는데, 접근자를 private으로 만들어서 외부에서 접근할 수 없도록 만든다.
  • @Builder
    • 앞서 설명한 Builder 패턴을 자동으로 생성해주는데, builderMethodName에 들어간 이름으로 빌더 메서드를 생성해준다.

      public class MainClass {
      
              public static void main(String[] args) {
                  // 빌더패턴을 통해 어떤 필드에 어떤 값을 넣어주는지 명확히 눈으로 확인할 수 있다!
                  TravelCheckList travelCheckList = TravelCheckList.builder(145L)
                          .passport("M12345")
                          .flightTicket("Paris flight ticket")
                          .creditCard("Shinhan card")
                          .internationalDriverLicense("1235-5345")
                          .travelerInsurance("Samsung insurance")
                          .build();
      
                  System.out.println("빌더 패턴 적용하기 : " + travelCheckList.toString());
      
              }
      
             // 결과
             // 빌더 패턴 적용하기 : TravelCheckList(id=1, passport=M12345, flightTicket=Paris flight ticket, creditCard=Shinhan card, internationalDriverLicense=1235-5345, travelerInsurance=Samsung insurance)
          }
  • 클래스 내부 builder 메서드 : 필수로 들어가야할 필드들을 검증하기 위해 만들었다. 꼭 id가 아니라도 해당 클래스를 객체로 생성할 때 필수적인 필드가 있다면 활용할 수 있다.
  • 만약 여기서 @AllArgsConstructor 어노테이션을 쓰지 않고 직접 생성자를 만든다면 다음과 같이 생성자 위에 @Builder 어노테이션을 붙여주면 된다.
  • 사실 @AllArgsConstructor 어노테이션이 편하다고 해서 무조건 좋은 것은 아니라 이 방법을 더 권장한다.
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Car {

    private String id;
    private String name;

    **@Builder**   // 생성자를 만든 후 그 위에 @Builder 애노테이션 적용
    public Car(String id, String name) {
        this.id = id;
        this.name = name;
    }
}
  • 적용은 다음과 같다.
public class CarImpl {

    private String id = "1";
    private String name = "carTest";

    Car car3 = Car.builder()
            .id(id)
            .name(name)
            .build();
}

사실 이 빌더 패턴에 대해서는 개발환경이나 언어적 특성에 따라 긍정적인 반응과 부정적인 반응이 갈리기는 한다.

모든 그렇듯, 무작정 사용하지 말고 한번 자신이 개발하는 전체적인 구조나 환경의 관점에서 장단점을 판단해보고 사용하는 것이 중요한 것 같다 !

profile
코딩 해라 스리스리 예스리 얍!

0개의 댓글