6 분 소요

빌더 패턴이란?

GoF 디자인 패턴 중 생성 패턴(Creational Pattern) 중 하나입니다. 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법입니다.

빌더 패턴은 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해 선택적인 값들에 대해서는 메서드를 통해 값을 입력 받은 후에 build() 메서드를 통해 최종적으로 인스턴스를 return 하는 방식입니다.

다음과 같은 경우 빌더 패턴을 고민해 볼 수 있습니다.

  • 생성자의 인자가 많은 경우
  • 생성자의 인자들 중 필수적 인자와 선택적 인자가 혼합되어 있는 경우
  • Immutable 객체(변경할 수 없는 객체)를 생성하고 싶은 경우

빌더 패턴 탄생 과정

public class Book {
    private Long id;		// 필수
    private String isbn;	// 필수
    private String title;	// 선택
    private String author;	// 선택
    private String pages;	// 선택
    private String category;// 선택
}

클래스의 인스턴스 변수가 다음과 같고 필수적으로 요구되는 변수 id, isbn과 그외 선택적인 변수들이 있다고 가정해보겠습니다.

점층적 생성자 패턴 (Telescoping Constructor Pattern)

필수 인자를 받는 생성자를 정의한 후 선택적 인자를 추가로 받는 생성자를 계속해서 정의하는 패턴입니다.

public class Book {
    private Long id;		// 필수
    private String isbn;	// 필수
    private String title;	// 선택
    private String author;	// 선택
    private String pages;	// 선택
    private String category;// 선택
    
public Book(Long id, String isbn) {
        this.id = id;
        this.isbn = isbn;
    }

    public Book(Long id, String isbn, String title) {
        this.id = id;
        this.isbn = isbn;
        this.title = title;
    }

    public Book(Long id, String isbn, String title, String author) {
        this.id = id;
        this.isbn = isbn;
        this.title = title;
        this.author = author;
    }

    public Book(Long id, String isbn, String title, String author, String pages) {
        this.id = id;
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.pages = pages;
    }

    public Book(Long id, String isbn, String title, String author, String pages, String category) {
        this.id = id;
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.pages = pages;
        this.category = category;
    }
}

문제점

  • 인자가 많을수록 생성자 개수가 많아집니다.

  • 생성자의 인자가 어떤 의미인지 파악하기 힘듭니다.

  • 가독성이 떨어집니다.

  • 만약 동일한 타입의 인자가 여러개인 경우 순서를 바꿔서 입력한 후 생성해도 문제없이 생성됩니다. 아래의 경우 문제없이 생성되지만 잘못된 값이 저장되었습니다.

    • new Book(1L, “isbn1234”, “Design Pattern”, “지은이”)
    • new Book(1L, “isbn1234”, “지은이”, “Design Pattern”)

JavaBeans 패턴

Setter 메서드로 각 속성의 값을 설정하는 방법입니다.

public class Book {
    private Long id;		// 필수
    private String isbn;	// 필수
    private String title;	// 선택
    private String author;	// 선택
    private String pages;	// 선택
    private String category;// 선택

    public void setId(Long id) {
        this.id = id;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public void setPages(String pages) {
        this.pages = pages;
    }

    public void setCategory(String category) {
        this.category = category;
    }
}

문제점

  • 가독성은 향상되지만 불변 객체를 만들 수 없습니다.
  • 클린 코드에서 getter, setter는 사용을 지양합니다. setter를 사용하지 않고 의미있는 변화가 필요하다면 관련 메서드를 생성하여 변화를 주어야 합니다.
  • setter를 통해 값을 변경할 경우 값의 변화를 추적하기 어렵습니다.
  • 메서드 호출이 필요한 인자만큼 이루어집니다.

Builder 패턴

필수적으로 요구되는 변수는 BookBuilder 클래스의 생성자를 통해 입력을 강제하고 선택적 인자는 BookBuilder 클래스의 메서드를 통해 추가로 입력받으면 됩니다. BookBuilder 클래스의 title 메서들를 확인해 보면 메서드 이름이 입력받고자 하는 변수 이름이라서 이름자체로 요구되는 값이 무엇인지 파악할 수 있고 BookBuilder를 반환함으로 메서드 체이닝이 가능하며 생성자와는 다르게 입력하는 값의 순서를 바꿔도 괜찮습니다(title 입력후 author 입력 == author 입력후 title 입력). 최종적으로 build 메서드를 호출해야 우리가 필요로하는 Book 객체가 생성이 됩니다.

public class Book {
    private Long id;		// 필수
    private String isbn;	// 필수
    private String title;	// 선택
    private String author;	// 선택
    private int pages;	// 선택
    private String category;// 선택   

    public static class BookBuilder {
        private Long id;		// 필수
        private String isbn;	// 필수
        private String title;	// 선택
        private String author;	// 선택
        private int pages;	// 선택
        private String category;// 선택

        // 필수 인자는 빌더 생성자로 받기
        public BookBuilder(Long id, String isbn) {
            this.id = id;
            this.isbn = isbn;
        }

        // 선택적 인자
        // 메서드 체이닝을 위한 BookBuilder 반환
        // 메서드 이름을 생성하고자 하는 클래스의 인스턴스 변수로 하여 파악하기 쉽도록 한다.
        public BookBuilder title(String title) {
            this.title = title;
            return this;
        }

        public BookBuilder author(String author) {
            this.author = author;
            return this;
        }

        public BookBuilder pages(int pages) {
            this.pages = pages;
            return this;
        }

        public BookBuilder category(String category) {
            this.category = category;
            return this;
        }

        public Book build() {
            Book book = new Book();
            book.id = this.id;
            book.isbn = this.isbn;
            book.author = this.author;
            book.title = this.title;
            book.pages = this.pages;
            book.category = this.category;
            return book;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Book book = new Book.BookBuilder(1L, "isbn1234")
                .author("지은이")
                .pages(360)
                .category("CE")
                .title("Design Pattern").build();        
    }
}

장점

  • 가독성 개선: 메서드 이름자체가 속성정보의 이름을 반영하기 때문
  • immutable 객체 생성 가능
  • 인자 순서로부터 자유로워짐

아래 형태도 참고

빌더패턴

public class TourPlan {
    private String title; // 여행 제목
    private LocalDate startDate; // 출발 일
    private int nights; // 몇 박
    private int days; // 며칠
    private String whereToStay; // 어디서 머물지
    private List<DetailPlan> plans; // n일차 할 일

    public TourPlan() {

    }

    public TourPlan(String title, LocalDate startDate, int nights,
                    int days, String whereToStay, List<DetailPlan> plans) {

        this.title = title;
        this.startDate = startDate;
        this.nights = nights;
        this.days = days;
        this.whereToStay = whereToStay;
        this.plans = plans;
    }
}

public class DetailPlan {
    private int day;    // n일차
    private String plan;    // 할 일

    public DetailPlan(int day, String plan) {
        this.day = day;
        this.plan = plan;
    }
}

인터페이스인 TourPlanBuilder를 만들어주고 이를 구현하는 ConcreteBuilder인 DefaultTourBuilder를 구현합니다.

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

public class DefaultTourBuilder implements TourPlanBuilder {
    private String title; // 여행 제목
    private LocalDate startDate; // 출발 일
    private int nights; // 몇 박
    private int days; // 며칠
    private String whereToStay; // 어디서 머물지
    private List<DetailPlan> plans; // n일차 할 일

    @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, startDate, nights, days, whereToStay, plans);
    }
}

TourPlan 객체를 생성할 때 아래와 같은 방법으로 생성 할 수 있게됩니다.

TourPlanBuilder builder = new DefaultTourPlanBuilder();
TourPlan tourPlan = tourPlanBuilder.title("칸쿤 여행")
        .nightsAndDays(2, 3)
        .startDate(LocalDate.of(2020, 12, 9))
        .whereToStay("리조트")
        .addPlan(0, "체크인하고 짐 풀기")
        .addPlan(0, "저녁 식사")
        .getPlan();

Director를 적용하면 클라이언트 코드가 더 짧아질 수 있습니다.

public class TourDirector {
    private final 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();
    }
}

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

        director = new TourDirector(new DefaultTourBuilder());
        TourPlan tourPlan2 = director.longBeachTrip();

        System.out.println(tourPlan1);
        System.out.println(tourPlan2);
    }
}

이처럼 디렉터를 사용할 경우 내부에 빌더를 설정해 주는데 다양한 빌더를 사용할 수 있어서 기능변경에 용이합니다. 하지만 클라이언트에서 디렉터와 빌더를 반드시 생성해야 하고 구조가 기존에 비해 복잡해진다는 단점이 있습니다.

lombok을 이용한 빌더 패턴

빌더 패턴을 위한 코드 작성이 번거롭게 느껴질 수 있습니다. 이는 롬복의 @Builder 어노테이션을 통해 해결 가능합니다.

reference

참고1

참고2

댓글남기기