[JAVA] Builder 패턴을 알아보자

0. 시작하며

저번에 이어서 Gof 생성 패턴 중 하나인 Builder 패턴을 알아보겠습니다. 알아 보면서 개인적으로 팩토리 메서드 패턴과 유사하다고 느꼈지만 목적에 있어서 차이점을 보여주는거같습니다. 두 패턴의 공통점과 차이점을 생각해보면 더 좋을거 같습니다.

 

1. 기존 코드

1. Client

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, "체크아웃");
    }
}

 

위와 같은 여행 생성 로직이 있다고 가정해봅시다. 여행을 짤때 우리는 많은 옵션을 줄 수 있습니다. 계획적인 사람은 더 잘게 쪼개서 시간단위로 계획을 짤 수도 있지만 즉흥적으로 여행을 간다면 목적지만 정할 수도 있죠.

 

이렇게 된다면 옵션에따라 반복되는 코드가 많아지므로 리팩토링이 매우 필요하게됩니다. 이때 사용할 수 있는 방법이 Builder 패턴입니다.

2. Builder 패턴 적용

1. Builder Interface

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();

}

 

팩토리 메소드 패턴과 비슷(?) 하게 인터페이스를 통해 추상화를 해줍니다. 이때 반환형을 선언하여 내부 프로세스 순서를 개발자의 의도대로 강제할 수 있고 Client 단계에서 체이닝방식으로 Bulder 패턴을 사용할 수 있습니다. 또한, 각 추상메서드의 구현체에 필요한 로직을 분산해서 처리하므로 가독성을 올려주고 복잡한 로직으로 실수할 여지가 줄어들 수 있습니다.

 

이 Bulider를 여러개 만들어서 VIP를 위한 여행, 국내 여행, 패키지 여행 등등 다양하게 여행 계획 인스턴스를 확장할 수 있습니다.

 

2. 추상 메서드 구현체

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);
    }
}

 

여러 구현 방식이 있지만 위처럼 내부에 필드를 선언하고 각 추상 메서드를 구현하는 방식이 있습니다. 이때 TourPlan의 필드가 중복되는게 싫다면 TourPlan 객체를 필드로 선언하고 사용할 수 있습니다. 이럴땐 호출될때 마다 매번 new 를 통해 선언되도록 해주는것이 안전합니다.

 

3.Client

public class App {
    public static void main(String[] args) {
        TourPlanBuilder builder = new DefaultTourBuilder();
        TourPlan plan = builder.title("칸쿤 여행")
                .nightsAndDays(2,3)
                .startDate(LocalDate.of(2024,3,24))
                .whereToStay("리조트")
                .addPlan(0,"체크인")
                .addPlan(0,"저녁 식사")
                .getPlan();

        TourPlan plan2 = builder.title("롱비치")
            .nightsAndDays(2,3)
            .startDate(LocalDate.of(2024,4,24))
            .getPlan();
    }
}

 

이렇게 메서드 체이닝 방식으로 생성할 수 있어서 가독성이 많이 좋아진 모습을 볼 수 있습니다. 대신 Builder 객체를 생성해야하므로 기존대비 오버헤드는 증가하는 Trade-off가 발생합니다.

 

만약 위 여행 계획이 패키지 상품 처럼 자주 사용되는 상품이라면 Director를 추가로 만들어 Client코드를 더욱 간단하게 할 수 있습니다.

 

4.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(2024,3,24))
                .whereToStay("리조트")
                .addPlan(0,"체크인")
                .addPlan(0,"저녁 식사")
                .getPlan();
    }

    public TourPlan longBeachTrip() {
        return tourPlanBuilder.title("롱비치")
	            .nightsAndDays(2,3)
	            .startDate(LocalDate.of(2024,4,24))
	            .getPlan();
    }
}

 

 

변경된 client

public class App {

    public static void main(String[] args) {
        TourDirector director = new TourDirector(new DefaultTourBuilder());
        TourPlan plan = director.cancunTrip();
        TourPlan plan2 = director.longBeachTrip();
    }
}

 

 

3. 장점과 단점

장점

  • client 코드를 간단하게 만들어줄 수 있습니다.
  • 각 필드에 기본형을 지정하거나 null을 파라미터로 넣지 않아도 객체를 생성할 수 있습니다.
  • 인터페이스 추상메서드의 반환타입을 통해서 생성 프로세스의 순서를 강제할 수 있습니다. 
  • 생성 로직이 복잡해진다면 여러 생성자를 만들어야하거나, 생성자 하나에 담기는 생성 로직이 굉장히 복잡할 수 있는데 분산해서 처리하는 방식을 통해 복잡성을 낮추고, 가독성을 향상시킬 수 있습니다. ( 개발자의 실수 방지 )
  • 빌더를 다양하게 구사함으로서 같은 프로세스를 거쳐도 다양한 객체를 만들 수 있도록 할 수 있으므로 객체의 확장성을 꾀할 수 있습니다.
  • 유효하지 않은 객체를 만들지 못하게 안전장치를 마련하기 용이합니다.

단점

  • class구조가 좀 복잡해지는 단점이 있습니다. (디자인패턴들의 공통적인 단점)
  • 객체를 더 만들어야하므로 성능상의 Trade-off가 발생합니다.

4. Lombok의 Builder

@Builder
public class LombokExample {

    private String title;

    private int nights;

    private int days;
    public static void main(String[] args) {
    	LombokExample trip = LombokExample.builder()
            .title("여행")
            .nights(2)
            .days(3)
            .build();
	}

}

 

Spring 프로젝트에서 많이 사용되는 Lombok의 경우 어노테이션으로 Builder 패턴을 쉽게 구현할 수 있습니다.

 

5. 팩토리 메서드 패턴 vs Builder 패턴

공통점

Gof 생성 패턴으로서 Interface를 통해 추상화하고 Client코드의 가독성을 올려주지만 Class 구조가 복잡해질 수 있고 내부로직을 확인하기위해 개발자가 한번 더 들어가서 확인해야합니다.

 

차이점

패턴 사용의 목적에서 가장 큰 차이가 있습니다. 팩토리 메서드 패턴은 일정한 범위안에 다양한 객체를 만들려고 사용합니다. 또한 생성 로직중 공통되는 부분을 같이 사용하고 구체적인 로직은 서브클래스에 위임합니다.

예를들어, Phone 에 해당하는 galaxy, iPhone, 화웨이 생성시에 사용하는게 유리합니다 

 

빌더 패턴의 경우 복잡한 객체 생성 프로세스에 사용됩니다. 각 프로세스 혹은 마지막 단계에 유효성 검사를 수행할 수 있고 각 단계에 알맞는 로직을 분산해서 처리하게됩니다.즉 비교적 복잡한 객체를 만들때 사용되는게 좋습니다. 

 

하지만 두 패턴을 양자 택일의 관점에서 바라보기보다 적절하게 사용할곳을 판단해서 장점을 극대화하는 방식의 개발이 좋을거같습니다.

 

6. 레퍼런스

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

 

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

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

www.inflearn.com

 

https://stackoverflow.com/questions/757743/what-is-the-difference-between-builder-design-pattern-and-factory-design-pattern

 

What is the difference between Builder Design pattern and Factory Design pattern?

What is the difference between the Builder design pattern and the Factory design pattern? Which one is more advantageous and why ? How do I represent my findings as a graph if I want to test and

stackoverflow.com

https://www.baeldung.com/cs/builder-pattern-vs-factory-pattern

 

 

'Java' 카테고리의 다른 글

[JAVA] 팩토리 메서드 패턴  (0) 2024.03.21
[Java] ConcurrentHashMap사용하기  (0) 2023.12.24
[Java] CompletableFuture 사용하기  (0) 2023.12.24
[Java] 디자인 패턴과 싱글톤  (0) 2023.09.06
객체 지향 프로그래밍  (0) 2023.08.30