Builder Pattern

2026. 6. 6. 22:35·Backend/Spring Boot

1. 빌더 패턴 탄생 배경

빌더 패턴이 왜 필요한지 이해하려면 그 이전의 객체 생성 방식들의 문제점을 먼저 알아야 합니다.

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

필드가 많은 객체를 생성할 때, 필수 파라미터만 받는 생성자부터 모든 파라미터를 받는 생성자까지 여러 개를 오버로딩으로 쌓아올리는 방식입니다.

class NutritionFacts {
    private final int servingSize;   // 필수
    private final int servings;      // 필수
    private final int calories;      // 선택
    private final int fat;           // 선택
    private final int sodium;        // 선택

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
    }
}

// 사용
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35);

문제점: 파라미터 순서를 외워야 하고, 어떤 값이 어떤 필드인지 가독성이 떨어집니다. 파라미터 수가 늘어날수록 생성자가 폭발적으로 증가합니다.


1-2. 자바 빈(Java Beans) 패턴

기본 생성자로 객체를 생성하고, setter 메서드로 각 필드를 초기화하는 방식입니다.

class NutritionFacts {
    private int servingSize;
    private int servings;
    private int calories;
    private int fat;
    private int sodium;

    // 기본 생성자 + setter
    public void setServingSize(int val) { servingSize = val; }
    public void setServings(int val)    { servings = val; }
    public void setCalories(int val)    { calories = val; }
    public void setFat(int val)         { fat = val; }
    public void setSodium(int val)      { sodium = val; }
}

// 사용
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);

문제점:

  • 일관성(Consistency) 붕괴: 객체가 완전히 초기화되기 전에 사용될 수 있습니다.
  • 불변성(Immutability) 불가: setter를 열어두면 외부에서 언제든 상태를 바꿀 수 있습니다.

1-3. 빌더(Builder) 패턴

위 두 방식의 장점을 합친 패턴입니다. 생성자의 안전성 + 세터의 가독성을 모두 취합니다.

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100)
        .sodium(35)
        .fat(0)
        .build();

2. 빌더 패턴 구조

2-1. 빌더 클래스 구현하기

class NutritionFacts {
    private final int servingSize;   // 필수
    private final int servings;      // 필수
    private final int calories;      // 선택
    private final int fat;           // 선택
    private final int sodium;        // 선택

    // private 생성자 → Builder를 통해서만 생성 가능
    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings    = builder.servings;
        this.calories    = builder.calories;
        this.fat         = builder.fat;
        this.sodium      = builder.sodium;
    }

    // 정적 내부 클래스로 Builder 정의
    public static class Builder {
        // 필수 파라미터
        private final int servingSize;
        private final int servings;

        // 선택 파라미터 (기본값 세팅)
        private int calories = 0;
        private int fat      = 0;
        private int sodium   = 0;

        // 필수 파라미터는 Builder 생성자에서 받음
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }

        // 선택 파라미터는 메서드 체이닝으로 설정
        public Builder calories(int val) {
            calories = val;
            return this; // 자기 자신을 반환 → 체이닝 가능
        }

        public Builder fat(int val) {
            fat = val;
            return this;
        }

        public Builder sodium(int val) {
            sodium = val;
            return this;
        }

        // 최종 객체 생성
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}

2-2. 빌더 클래스 실행하기

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) // 필수값
        .calories(100)   // 선택값
        .sodium(35)
        .fat(0)
        .build();        // 최종 객체 생성

2-3. 빌더 패턴 네이밍 형식

스타일 예시

일반 setter 스타일 .setCalories(100)
Effective Java 스타일 .calories(100) (권장)
with 스타일 .withCalories(100)

실무에서는 Effective Java 스타일(파라미터명과 동일한 메서드명)이 가장 많이 쓰입니다.


Builder 패턴 장단점 총정리

1. 빌더 패턴 장점

1. 객체 생성 과정을 일관된 프로세스로 표현

build() 호출 시점에 한 번에 객체가 생성되므로, 중간 상태가 존재하지 않습니다. 객체 생성의 시작과 끝이 명확합니다.

2. 디폴트 매개변수 생략을 간접적으로 지원

Java는 기본 파라미터 문법이 없지만, 빌더에서 선택 파라미터에 기본값을 세팅해두면 호출 측에서 생략이 가능합니다.

// fat, sodium 생략 → 기본값 0 적용
NutritionFacts f = new NutritionFacts.Builder(240, 8).calories(100).build();

3. 필수 멤버와 선택적 멤버를 분리 가능

Builder 생성자에 필수값을 받고, 메서드 체이닝으로 선택값을 받음으로써 컴파일 타임에 필수값 누락을 방지할 수 있습니다.

4. 객체 생성 단계를 지연할 수 있음

Builder 자체를 저장해뒀다가, 나중에 필요한 시점에 build()를 호출할 수 있습니다.

NutritionFacts.Builder builder = new NutritionFacts.Builder(240, 8).calories(100);
// ... 다른 로직 실행 후
NutritionFacts facts = builder.sodium(35).build(); // 지연 생성

5. 초기화 검증을 멤버별로 분리

각 setter 메서드 안에서 개별 유효성 검사를 할 수 있습니다.

public Builder calories(int val) {
    if (val < 0) throw new IllegalArgumentException("calories는 음수일 수 없습니다.");
    calories = val;
    return this;
}

6. 멤버에 대한 변경 가능성 최소화를 추구

외부 객체에서 final 필드로 선언하고 Builder를 통해서만 초기화하므로 불변 객체(Immutable Object) 를 자연스럽게 만들 수 있습니다.


2. 빌더 패턴 단점

1. 코드 복잡성 증가

필드마다 Builder 메서드를 따로 만들어야 하므로 클래스 코드량이 늘어납니다. 필드가 4개 이하라면 점층적 생성자가 더 간단할 수 있습니다.

2. 생성자보다 성능이 떨어진다

Builder 객체를 먼저 만들고, 그 Builder 객체를 기반으로 최종 객체를 만드는 2단계 생성 구조이므로 성능에 민감한 상황에서는 미세하게 불리합니다. (단, 대부분의 상황에서 무시할 수 있는 수준)

3. 지나친 빌더 남용은 금지

필드가 2~3개 수준의 단순한 클래스에 빌더를 도입하면 오히려 코드만 복잡해집니다. 필드 수가 많고(4개 이상), 선택적 필드가 있을 때 사용하는 게 적절합니다.


Builder 디자인 패턴 종류

1. 심플 빌더 패턴 (Effective Java)

앞서 설명한 것처럼 대상 클래스 내부에 정적 Builder 클래스를 정의하는 방식입니다. Java 실무에서 가장 흔하게 볼 수 있는 형태입니다.

Simple 빌더 패턴 구현하기

class Person {
    private final String name;    // 필수
    private final int age;        // 필수
    private final String email;   // 선택
    private final String phone;   // 선택

    private Person(Builder builder) {
        this.name  = builder.name;
        this.age   = builder.age;
        this.email = builder.email;
        this.phone = builder.phone;
    }

    public static class Builder {
        private final String name;
        private final int age;
        private String email = "";
        private String phone = "";

        public Builder(String name, int age) {
            this.name = name;
            this.age  = age;
        }

        public Builder email(String val) { email = val; return this; }
        public Builder phone(String val) { phone = val; return this; }
        public Person build() { return new Person(this); }
    }
}

// 사용
Person p = new Person.Builder("홍길동", 30)
        .email("hong@example.com")
        .phone("010-1234-5678")
        .build();

2. 디렉터 빌더 패턴 (GOF)

GOF(Gang of Four)의 원조 빌더 패턴입니다. Effective Java 방식보다 구조가 복잡하지만, "어떤 순서로 어떻게 조립할지"를 Director가 담당한다는 점이 핵심입니다.

Director 빌더 패턴 구조

Client
  └── Director          (조립 순서/알고리즘을 관리)
        └── Builder (인터페이스)
              ├── ConcreteBuilderA  (구체적인 조립 방법 A)
              └── ConcreteBuilderB  (구체적인 조립 방법 B)

Director 빌더 패턴 구현하기

// 최종 제품
class Computer {
    private String cpu;
    private String ram;
    private String storage;

    public void setCpu(String cpu)         { this.cpu = cpu; }
    public void setRam(String ram)         { this.ram = ram; }
    public void setStorage(String storage) { this.storage = storage; }

    @Override
    public String toString() {
        return "Computer{cpu='" + cpu + "', ram='" + ram + "', storage='" + storage + "'}";
    }
}

// Builder 인터페이스
interface ComputerBuilder {
    void buildCpu();
    void buildRam();
    void buildStorage();
    Computer getResult();
}

// Concrete Builder A → 게이밍 PC
class GamingComputerBuilder implements ComputerBuilder {
    private Computer computer = new Computer();

    public void buildCpu()     { computer.setCpu("Intel i9"); }
    public void buildRam()     { computer.setRam("64GB DDR5"); }
    public void buildStorage() { computer.setStorage("2TB NVMe SSD"); }
    public Computer getResult() { return computer; }
}

// Concrete Builder B → 사무용 PC
class OfficeComputerBuilder implements ComputerBuilder {
    private Computer computer = new Computer();

    public void buildCpu()     { computer.setCpu("Intel i5"); }
    public void buildRam()     { computer.setRam("16GB DDR4"); }
    public void buildStorage() { computer.setStorage("512GB SSD"); }
    public Computer getResult() { return computer; }
}

// Director → 조립 순서를 책임짐
class Director {
    private ComputerBuilder builder;

    public Director(ComputerBuilder builder) {
        this.builder = builder;
    }

    public Computer construct() {
        builder.buildCpu();
        builder.buildRam();
        builder.buildStorage();
        return builder.getResult();
    }
}

// 사용
Director director = new Director(new GamingComputerBuilder());
Computer gamingPC = director.construct();
System.out.println(gamingPC);
// Computer{cpu='Intel i9', ram='64GB DDR5', storage='2TB NVMe SSD'}

GOF의 빌더 패턴은 여러 디자인 패턴의 짬뽕

패턴 역할

전략 패턴 Director가 Builder 인터페이스에 의존 → 런타임에 Builder 교체 가능
템플릿 메서드 패턴 Director의 construct()가 조립 순서(알고리즘)를 정의
팩토리 패턴 getResult()가 최종 객체를 생성해 반환

3. Lombok의 @Builder

Lombok 라이브러리를 사용하면 빌더 패턴 코드를 자동으로 생성할 수 있습니다.

빌더 어노테이션 구현하기

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class Person {
    private String name;
    private int age;
    private String email;
    private String phone;
}

// 사용
Person p = Person.builder()
        .name("홍길동")
        .age(30)
        .email("hong@example.com")
        .build();

@Builder 하나만 붙이면 Lombok이 컴파일 시점에 Builder 클래스를 자동 생성해줍니다.

필수 파라미터 빌더 구현하기

@Builder는 기본적으로 모든 필드를 선택적으로 만들기 때문에, 필수 필드 강제를 위해 @NonNull을 활용합니다.

@Getter
@Builder
public class Person {
    @NonNull
    private String name;   // null이면 NullPointerException 발생
    @NonNull
    private int age;
    private String email;  // 선택
    private String phone;  // 선택
}

또는 @Builder + 커스텀 생성자를 조합하는 방법도 있습니다.

@Getter
@Builder(builderMethodName = "hiddenBuilder") // 기본 빌더를 숨김
public class Person {
    private final String name;
    private final int age;
    private String email;
    private String phone;

    // 필수 파라미터를 받는 커스텀 정적 메서드
    public static PersonBuilder builder(String name, int age) {
        return hiddenBuilder().name(name).age(age);
    }
}

// 사용
Person p = Person.builder("홍길동", 30)
        .email("hong@example.com")
        .build();

실무에서 찾아보는 Builder 패턴

1. Java

StringBuilder

가장 대표적인 빌더 패턴 활용 사례입니다. 문자열을 단계적으로 쌓아 올린 후 최종적으로 toString()으로 결과물을 만듭니다.

String result = new StringBuilder()
        .append("Hello")
        .append(", ")
        .append("World")
        .append("!")
        .toString();
// "Hello, World!"

StreamBuilder

Stream.Builder도 같은 개념입니다. 요소를 하나씩 추가하다 build()를 호출해 Stream을 완성합니다.

Stream.Builder<String> streamBuilder = Stream.builder();
streamBuilder.add("A");
streamBuilder.add("B");
streamBuilder.add("C");
Stream<String> stream = streamBuilder.build();
stream.forEach(System.out::println);

2. Spring Framework

Spring에서는 다양한 곳에서 빌더 패턴이 활용됩니다.

// UriComponentsBuilder
URI uri = UriComponentsBuilder
        .fromUriString("<https://api.example.com>")
        .path("/users/{id}")
        .queryParam("active", true)
        .buildAndExpand(42)
        .toUri();

// MockMvcRequestBuilders (테스트)
mockMvc.perform(
    MockMvcRequestBuilders.get("/api/users")
        .contentType(MediaType.APPLICATION_JSON)
        .header("Authorization", "Bearer token")
);

// ResponseEntity (Spring MVC 응답 빌더)
return ResponseEntity.status(HttpStatus.CREATED)
        .header("Location", "/users/1")
        .body(createdUser);

정리

구분 특징 사용 시점

심플 빌더 (Effective Java) 내부 정적 클래스, 불변 객체 필드 많고 선택적 파라미터 많을 때
디렉터 빌더 (GOF) Director + Builder 분리, 조립 알고리즘 캡슐화 조립 순서가 중요하고 여러 변형이 필요할 때
Lombok @Builder 보일러플레이트 자동 생성 실무 빠른 개발, 단 필수값 강제엔 추가 작업 필요

실무에서는 Lombok @Builder 또는 Effective Java 스타일이 압도적으로 많이 쓰이며, GOF 방식은 프레임워크나 라이브러리 설계 레벨에서 주로 등장합니다.

'Backend > Spring Boot' 카테고리의 다른 글

DAO, DTO, VO, Entity  (0) 2026.06.06
Bean Scope  (0) 2026.06.06
SpringBoot 내부동작  (0) 2026.06.06
[Spring Boot] 공식문서 번역 - RESTful 웹 빌드하기  (5) 2025.02.04
[Spring Boot] 스프링을 처음할때 보기 좋은 공식문서 순서  (3) 2025.01.08
'Backend/Spring Boot' 카테고리의 다른 글
  • DAO, DTO, VO, Entity
  • Bean Scope
  • SpringBoot 내부동작
  • [Spring Boot] 공식문서 번역 - RESTful 웹 빌드하기
mjwoon
mjwoon
  • mjwoon
    모시깽이 개발자
    mjwoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (38)
      • Study notes (24)
        • 운영체제론 (10)
        • 컴퓨터 네트워크 (0)
        • 자연어처리 (7)
        • 데이터베이스 (0)
        • 객체지향개발론 (0)
        • 딥러닝 (0)
        • 데이터사이언스 (7)
      • Frontend (2)
        • React (2)
      • Backend (6)
        • Spring Boot (6)
      • Algorithm (5)
      • 핸즈온 머신러닝 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    JavaScript
    알고리즘
    자바스크립트
    자료구조
    재귀
    백준
    슬라이딩윈도우
    Nodejs
    vscode
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
mjwoon
Builder Pattern
상단으로

티스토리툴바