backgroundbackground
Sidebar Background Day
Sidebar Background Night
YoonCarrot

전체 (52)

    • Blog(4)
    • Design Pattern(20)
    • Clean Code(2)
    • Refactoring(5)
    • Augmented Coding(1)
    • 아키텍처(1)
    • Typescript(2)
    • Algorithm(1)
    • 운영체제(4)
    • Network(4)
    • React(6)
    • NextJs(1)
    • 웹 최적화(1)
맛있는 페이지 준비중...맛있는 페이지 준비중...
디자인 패턴빌더 패턴

[ Programming > Design Pattern ]

[디자인 패턴] 빌더 패턴

 Carrot Yoon
 2025-11-14
 14

빌더 패턴(Builder Pattern)

빌더 패턴은 복잡하게 여러 단계로 나눠는 객체 생성 과정을 캡슐화하고 싶을때 쓰는 생성 패턴이에요. 객체 생성 로직이 복잡한 경우나 동일한 구조에서 서로 다른 설정으로 객체를 만들고 싶은 경우 유용해요. SQL을 코드로 작성하는 경우, 프론트엔드는 zod라는 타입 검증을 만드는 라이브러리에서 많이 사용되요.

빌더 패턴 구조 (Director 사용하는 빌더패턴)

Mermaid Chart - Create complex, visual diagrams with text.-2025-11-14-100720.webp

Director 방식을 사용하는 빌더 패턴은 복잡한 생성 과정을 제어하는 Director, 제품 생성에 필요한 방법을 추상화한 인터페이스인 Builder, Builder 인터페이스를 구현한 ConcreteBuilder, 그리고 결과물인 Product로 구성되요.

  1. Director

    • 역할: Builder를 사용해서 객체 생성 순서와 과정을 제어하는 역할이에요. Client 대신 복잡한 생성 로직을 관리해요.

    • 특징: 어떤 ConcreteBuilder가 사용될지 모르지만, Builder 인터페이스만 알고 있으면 일관된 방식으로 객체를 만들 수 있어요.

    • 예시: 건축 감독 → 어떤 집을 짓든 "기초 → 골조 → 벽 → 지붕" 순서로 지시해요.

  2. Builder

    • 역할: 복잡한 객체를 생성하는데 필요한 단계들을 추상 메서드로 정의하는 인터페이스에요.

    • 특징: Product를 만드는 최소 기준을 정의하고, ConcreteBuilder들이 따라야 할 공통 API를 제공해요.

    • 예시: 집 짓기 설계도 → buildFoundation(), buildWalls(), buildRoof() 등의 단계를 정의해요.

  3. ConcreteBuilder

    • 역할: Builder 인터페이스를 실제로 구현하여 특정 타입의 Product를 만드는 클래스에요.

    • 특징: Director를 몰라도 Builder 인터페이스만 알고 구현하면 되요. 각 단계에서 자신만의 방식으로 Product를 조립해요.

    • 예시: 목조주택 빌더, 콘크리트 빌더 등 → 같은 단계지만 재료와 방식이 달라요.

  4. Product

    • 역할: 빌더를 통해 최종적으로 만들어지는 복잡한 객체에요.

    • 특징: 여러 부품과 속성으로 구성되어 있으며, 한번에 만들기 어려운 복잡한 구조를 가져요. ConcreteBuilder마다 다른 타입의 Product가 만들어질 수 있어요.

    • 예시: 완성된 집 → 기초, 벽, 지붕 등 여러 부분이 조립된 최종 결과물이에요.

다양한 빌더 패턴

빌더 패턴은 필요에 따라 다양하게 활용되요. 여러 빌더 패턴을 소개할게요.

상황

방법

이유

순서가 복잡하고 여러 변형이 있는 경우

Director

(복잡한 또는 순서있는)생성 로직을 재사용 가능

순서는 중요하지만 유연성도 필요한 경우

Builder 내부 검증

자유롭게 사용하되 안전하게

순서를 절대적으로 강제해야 하는 경우

Step Builder

컴파일 타임에 순서 보장

순서가 중요하지 않은 경우

일반 Builder

컴퓨터 조립처럼 순서 무관

순서는 정해져 있지만 호출은 자유롭게 하고 싶은 경우,

비싼 함수의 실행 시점을 조절하고 싶은 경우

명령 큐잉 (지연 실행)

명령 저장 후 build()에서 최적화

예시 - 빌더 패턴

아래 예시에서는 빌더 패턴의 사용법들에 대해서만 다양하게 이야기하고 있어요. 하지만 명심할 점은 클래스 다이어그램을 보면, Builder를 인터페이스나 추상 클래스로 빼고, 이를 구현하는 다양한 빌더들을 만들 수 있음도 잊지마세요. 예를들면 아래 Director 빌더 패턴에서 MealBuilder를 추상 클래스로 빼고, ExpensiveMealBuilder, CheapMealBuilder과 같이 여러 빌더를 만들어 확장성을 가질 수도 있어요.

1. Director 빌더 패턴

Director 빌더 패턴을 통해 다양한 식사를 구성하는 디렉터를 만들어 볼거에요. 어린이를 위한 식사 준비, 채식주의자를 위한 식사 준비를 해야한다고 할 때, 각 식사가 Director에서 정한 순서대로 준비될 수 있도록 만들거에요.

보통 여러 버전의 비즈니스 로직이 계속 생기고, 순서나 처리 알고리즘이 각 로직에서 다르고 지켜야할 경우 주로 사용해요.

// Builderclass MealBuilder {  constructor() {    this.meal = {};  }  addMain(main) {    this.meal.main = main;    return this;  }  addSide(side) {    this.meal.side = side;    return this;  }  addDrink(drink) {    this.meal.drink = drink;    return this;  }  build() {    return this.meal;  }}// Director (순서 로직 재사용)class MealDirector {  static createKidsMeal(builder) {    return builder      .addMain("Mini Burger")      .addSide("Fries")      .addDrink("Apple Juice")      .build();  }  static createVeganMeal(builder) {    return builder      .addMain("Tofu Bowl")      .addSide("Salad")      .addDrink("Water")      .build();  }}// 사용const kids = MealDirector.createKidsMeal(new MealBuilder());const vegan = MealDirector.createVeganMeal(new MealBuilder());

2. 일반 빌더 패턴 + 내부 검증

이 패턴은 빌더 패턴에 유저가 실수하지 않도록 내부 검증을 build 시점에 하고 결과를 리턴하는 패턴이에요. 만약 내부 검증을 하지 않는다면 일반 빌더 패턴과 같다고 보면 되요.

class UserBuilder {  constructor() {    this.data = {};  }  setName(name) {    this.data.name = name;    return this;  }  setAge(age) {    this.data.age = age;    return this;  }  setEmail(email) {    this.data.email = email;    return this;  }  build() {    // 내부 검증    if (!this.data.name) throw new Error("name is required");    if (!this.data.email) throw new Error("email is required");    return this.data;  }}// 사용const user = new UserBuilder()  .setName("Carrot")  .setEmail("test@test.com")  .build();console.log(user);

3. 스텝 빌더 패턴

이 패턴은 순서를 타입 레벨부터 강제하는 패턴이에요. 철저히 의도한대로 객체가 생성되도록 하고싶을 때 유용해요. 좀 더 응용하면, 분기형 Step 구조를 갖게 만들 수도 있어요. 예를 들면 설문조사에서 A와B라는 응답이 있을 때 A를 선택하면 A와 관련된 질문에 대해서만 답하게 만들 수 있는 거에요.

interface Step1 {  setName(name: string): Step2;}interface Step2 {  setAge(age: number): Step3;}interface Step3 {  setEmail(email: string): FinalStep;}interface FinalStep {  build(): User;}type User = {  name: string;  age: number;  email: string;};class UserBuilder implements Step1, Step2, Step3, FinalStep {  private user: Partial<User> = {};  // Step 1  setName(name: string): Step2 {    this.user.name = name;    return this;  }  // Step 2  setAge(age: number): Step3 {    this.user.age = age;    return this;  }  // Step 3  setEmail(email: string): FinalStep {    this.user.email = email;    return this;  }  // Final  build(): User {    return this.user as User;  }}const user = new UserBuilder()  .setName("Carrot")   // OK → Step1  .setAge(25)          // OK → Step2  .setEmail("a@a.com") // OK → Step3  .build();            // OK → FinalStep

4. 빌더 패턴 With Lazy Execution(지연 실행)

위 코드들을 보면 build()시점에 객체를 return하기 때문에 build() 시점에 객체를 만든 것 처럼 보이지만, 중간 중간에 이미 변수를 필드에 할당하고 있어요. 하지만 이 패턴은 객체 생성이나 복잡한 함수 실행을 build 시점까지 미루는 방법이에요. 이 방법을 통해 비싼 연산(복잡한 함수)의 의미 없는 실행을 막거나 딜레이 시킬 수 있고, 조건부 생성같은 복잡한 비즈니스 로직을 적용한 유연한 생성 과정을 구현할 수 있어요.

class QueryBuilder {  constructor() {    this.queue = [];  }  select(columns) {    this.queue.push({ type: "select", columns });    return this;  }  where(condition) {    this.queue.push({ type: "where", condition });    return this;  }  orderBy(column) {    this.queue.push({ type: "orderBy", column });    return this;  }  build() {    // 정해진 순서로 정렬    const order = ["select", "where", "orderBy"];    return this.queue      .sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type))      .map(cmd => cmd);  }}// 사용 — 아무 순서로 호출해도 됨const q = new QueryBuilder()  .where("age > 20")  .select(["name", "age"])  .orderBy("age DESC")  .build();

이 패턴은 이해를 돕기 위해 자바 코드로도 한번 보여드릴 게요.

public class ComplexOperationResult {    private String finalOutput;    private ComplexOperationResult(String finalOutput) {        this.finalOutput = finalOutput;    }    public String getFinalOutput() {        return finalOutput;    }    public static class Builder {        private String inputData;        private List<Function<String, String>> transformations = new ArrayList<>();        public Builder withInput(String data) {            this.inputData = data;            return this;        }        public Builder addTransformation(Function<String, String> transformation) {            this.transformations.add(transformation);            return this;        }        public ComplexOperationResult build() {            // Lazy execution: build가 실행될 때만 변형이 일어남.            String processedData = inputData;            for (Function<String, String> transformation : transformations) {                processedData = transformation.apply(processedData);            }            return new ComplexOperationResult(processedData);        }    }}

빌더 패턴 특징

이제 빌더 패턴의 특징에 대해서 알아봐요.

  • 장점

    • 복합 객체의 생성을 캡슐화해요.

    • 팩토리 패턴과 달리 여러 단계에 걸쳐 다양한 절차로 조절하여 객체를 만들 수 있어요.

    • 생성자의 파라미터가 많을 때(6개, 7개...) 깔끔하게 처리할 수 있어요.

    • Director 패턴을 쓰면 조립 순서를 재사용할 수 있어요.

    • 가독성이 좋아저요.

    • 빌드 로직 테스트가 쉬워요.

  • 단점

    • 단순 객체 생성에는 오버엔지니어링이에요.

    • 많은 객체를 만들때는 오버헤드가 클 수 있어요. (new A()보다 느림)

    • 과도하게 복잡해질 수 있어요.

    • 객체를 만들기 위해 팩토리 패턴보다 더 많이 구조를 알아야 해요.

마무리

이번에는 빌더 패턴에 대해서 알아봤어요. 프론트엔드의 zod나 Spring의 @builder 어노테이션을 쓰면서 이 패턴에 대해서 많이 써봤을 것 같아요. 확실히 코드가 깔끔해지고, 단계 단계 확인하는 느낌이 나서 쓰기 좋았던거 같아요. 아무튼 이렇게 벌써 팩토리 패턴, 빌더 패턴, 싱글톤 패턴의 3개 생성 패턴을 배웠네요. 이제 프로토타입 패턴정도의 생성 패턴이 남았나요? 앞으로도 계속 열심히 달려봐요.