Ian's Archive 🏃🏻

Profile

Ian

Ian's Archive

Developer / React, SpringBoot ...

📍 Korea
Github Profile →
Categories
All PostsAlgorithm19Book1C1CI/CD2Cloud3DB1DesignPattern9ELK4Engineering1Front3Gatsby2Git2IDE1JAVA7JPA5Java1Linux8Nginx1PHP2Python1React9Security4SpatialData1Spring26
thumbnail

빌더 패턴 정리

DesignPattern
2025.10.22.

1. 빌더 패턴

  • 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법
  • 멤버 변수가 많고, 생성자가 많아질 경우 복잡하고 파악하기 힘든 생성과정이 생기는 것이므로, 이를 순차적이고 선언적인 객체 생성 프로세스로 바꾸는 방법

1

2. 빌더 패턴 구현 방법

  • 빌더 패턴 구현의 요점은, 메소드 체이닝
  • 메소드 체이닝으로 엔티티의 데이터를 세팅해주는 빌더라는 인터페이스를 만들어 준 후 계속 스스로를 반환하고, 마지막에 해당 객체를 반환토록 하는 것

구현 설명

  • APP : 클라이언트의 역할을 하는 객체
  • TourDirector : 빌더 패턴을 적용할 TourPlanBuilder를 목적에 맞게 미리 구현해둔 메소드의 집합을 가지는 객체
  • TourPlanBuilder : 빌더 패턴 적용한 interface
  • DefaultTourPalnBuilder : TourPlanBuilder의 구현체

TourPlanBuilder

복사
public interface TourPlanBuilder {

    //메서드 체이닝 사용 → 인터페이스에서 정의한 또 다른 기능을 사용하기 위해서
    TourPlanBuilder title(String title);
    TourPlanBuilder nightsAndDays(int nights, int days);
    TourPlanBuilder startDate(LocalDate localDate);
    TourPlanBuilder whereToStay(String whereToStay);
    TourPlanBuilder addPlan(int day, String plan);


    //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.days = days;
        this.nights = nights;

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

Client

  • 빌더 패턴을 적용해 객체 생성 가능
  • 빌더 패턴을 적용하면 선언적이고, 순차적인 구조로 객체 생성의 프로세스를 작성 가능
  • 또한 빌더패턴을 이용해서 초기 Builder 를 선언하는 부분을 이용한다면 파라미터를 이용해 강제적으로 데이터 세팅이 가능하도록 설계 가능
복사
public class App {

    public static void main(String[] args) {
        TourPlanBuilder builder = new DefaultTourBuilder();

        TourPlan plan = builder.title("칸쿤여행")
                .nightsAndDays(2, 3)
                .startDate(LocalDate.of(2020, 10, 2))
                .whereToStay("hotel")
                .addPlan(0, "저녁 식사")
                .addPlan(1, "아침 식사")
                .getPlan();

        TourPlan longBeachTrip = builder.title("롱비치")
                .startDate(LocalDate.now())
                .getPlan();

        TourPlan tourPlan = DefaultTourBuilder.Builder("title").getPlan();

        // Director를 사용해 프로세스가 구성되어 사용하는 코드로 바뀌게 된다.
        TourDirector director = new TourDirector(builder);

        TourPlan cancun = director.cancunTrip();
        TourPlan longBeach = director.longBeachTrip();
    }
}

TourDirector

복사
public class TourDirector {

    private TourPlanBuilder tourPlanBuilder;

    public TourDirector(TourPlanBuilder tourPlanBuilder) {
        this.tourPlanBuilder = tourPlanBuilder;
    }

    public TourPlan cancunTrip() {
        return tourPlanBuilder
                .title("칸쿤여행")
                .nightsAndDays(2, 3)
                .addPlan(0, "체크인하고 짐 풀기")
                .getPlan();
    }

    public TourPlan longBeachTrip() {
        return tourPlanBuilder
                .title("긴해변여행")
                .nightsAndDays(2, 3)
                .addPlan(0, "체크인하고 짐 풀기")
                .getPlan();
    }
}

빌더 패턴 네이밍 형식

  • 대표적으로 3가지 존재
    • 멤버이름()
    • set멤버이름()
    • with멤버이름()

3. 장점과 단점

장점

  1. 객체 생성 과정을 일관된 프로세스로 표현
  2. 디폴트 매개변수 생략을 간접적으로 지원
  • 디폴트 매개변수가 설정된 필드를 설정하는 메서드를 호출하지 않는 방식
  1. 필수 멤버와 선택적 멤버를 분리 가능
  2. 객체 생성 단계를 지연할 수 있음
  • 객체 생성을 단계별로 구성하거나 구성 단계를 지연하거나 재귀적으로 생성을 처리할 수 있다.
복사
java// 1. 빌더 클래스 전용 리스트 생성
List<StudentBuilder> builders = new ArrayList<>();

// 2. 객체를 최종 생성 하지말고 초깃값만 세팅한 빌더만 생성
builders.add(
    new StudentBuilder(2016120091)
    .name("홍길동")
);

builders.add(
    new StudentBuilder(2016120092)
    .name("임꺽정")
    .grade("senior")
);

builders.add(
    new StudentBuilder(2016120093)
    .name("박혁거세")
    .grade("sophomore")
    .phoneNumber("010-5555-5555")
);

// 3. 나중에 빌더 리스트를 순회하여 최종 객체 생성을 주도
for(StudentBuilder b : builders) {
    Student student = b.build();
    System.out.println(student);
}
  1. 초기화 검증을 멤버별로 분리
  2. 멤버에 대한 변경 가능성 최소화를 추구
  • 클래스 맴버 초기화를 Setter를 통해 구성하는 것은 매우 좋지 않은 방법이다.
    • 불변 객체와 연관이 있는데, 불변 객체는 오로지 읽기(get)메소드만을 제공하며 쓰기(set)은 제공하지 않는다. (자바의 final)
      • 불변 객체의 장점
      • 불변 객체는 Thread-Safe하여 동기화를 고려하지 않아도 된다.
      • 만일 가변 객체를 통해 작업을 하는 도중 예외(Exception)이 발생하면 해당 객체가 불안정한 상태에 빠질 수 있어 또 다른 에러를 유발할 수 있는 위험성이 있기 때문이다.
      • 불변 객체로 구성하면 다른 사람이 개발한 함수를 위험없이 이용을 보장할 수 있어 협업에도 유지보수에 유용하다.

즉, 빌더 패턴은 생성자 없이 어느 객체에 대해 ‘변경 가능성을 최소화’를 추구해 불변성을 갖게 해준다.

단점

  1. 코드 복잡성 증가
  2. 생성자 보다는 성능이 떨어진다.
  3. 지나친 빌더 남용은 금지
    • 필드 개수가 적고, 변경 가능성이 없는 경우라면 차라리 생성자나 정적 팩토리 메소드를 이용하는 것이 좋을 수 있다.

4. Lombok @Builder가 생성하는 코드

  • Lombok은 리플렉션을 이용해, 컴파일 시 해당 어노테이션이 등록된 위치에 맞춰 정의된 기능을 주입
  • @Builder가 적용된 객체는 위에서 살펴보았던 빌더패턴이 그대로 객체 내부 코드에 삽입된 걸 확인해 볼 수 있다.
복사
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;
    }

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

    public static class TourPlanBuilder {
        private String title;
        private int nights;
        private int days;
        private LocalDate startDate;
        private String whereToStay;
        private List<DetailPlan> plans;

        TourPlanBuilder() {
        }

        public TourPlanBuilder title(final String title) {
            this.title = title;
            return this;
        }

        public TourPlanBuilder nights(final int nights) {
            this.nights = nights;
            return this;
        }

        public TourPlanBuilder days(final int days) {
            this.days = days;
            return this;
        }

        public TourPlanBuilder startDate(final LocalDate startDate) {
            this.startDate = startDate;
            return this;
        }

        public TourPlanBuilder whereToStay(final String whereToStay) {
            this.whereToStay = whereToStay;
            return this;
        }

        public TourPlanBuilder plans(final List<DetailPlan> plans) {
            this.plans = plans;
            return this;
        }

        public TourPlan build() {
            return new TourPlan(this.title, this.nights, this.days, this.startDate, this.whereToStay, this.plans);
        }

        public String toString() {
            return "TourPlan.TourPlanBuilder(title=" + this.title + ", nights=" + this.nights + ", days=" + this.days + ", startDate=" + this.startDate + ", whereToStay=" + this.whereToStay + ", plans=" + this.plans + ")";
        }
    }
}

5. 실무에서 찾아보는 Builder 패턴

  • StringBuilder
    • 빌더에 해당하는 StringBuilder를 생성하고, 빌더가 제공하는 append 메서드로 파라미터를 구성하고, 최종적으로 toString을 호출해서 String 객체를 생성하는 일련의 과정이 빌더 패턴
  • StreamBuilder
    • Stream에 들어갈 요소를 add하고, 최종적으로 build를 호출해 stream 객체를 생성
  • Uricomponentsbuilder

Reference

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

Previous Post
추상 팩토리 정리
Next Post
프로토타입 패턴 정리
Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.