김코딩

빌더 패턴(Builder Pattern) 본문

디자인 패턴

빌더 패턴(Builder Pattern)

김코딩딩 2026. 1. 13. 18:47

디자인 패턴 시리즈의 네 번째 주제는 빌더 패턴(Builder Pattern)이다.

 

지금까지 본 패턴들이 "어떤 객체를 만들까?"에 집중했다면, 빌더 패턴"복잡한 객체를 어떻게 만들까?"에 대한 해답이다.

 

예를 들어, 피자를 주문한다고 생각해보자. 도우 종류, 소스, 토핑, 치즈 종류, 사이즈 등 선택할 게 정말 많다.

생성자로 이 모든 걸 받으면 new Pizza(도우, 소스, 토핑1, 토핑2, 토핑3, 치즈, 사이즈, 굽기정도, 엣지)처럼 엄청 길어진다.

빌더 패턴은 이런 복잡한 객체를 단계별로 만들 수 있게 해준다.

 

이번 글에서는 빌더 패턴무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.


빌더 패턴의 정의

빌더 패턴(Builder Pattern)은 복잡한 객체의 생성 과정과 표현 방법을 분리하여, 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다.

 

핵심 특징:

  1. 복잡한 객체를 단계별로 생성한다
  2. 생성 과정을 메서드 체이닝으로 연결한다
  3. 필수 값과 선택 값을 명확하게 구분한다
  4. 불변 객체(Immutable Object) 생성에 적합하다

왜 빌더 패턴이 필요할까?

피자 주문 시스템을 개발한다고 가정해보자.

방법 1: 점층적 생성자 패턴 (문제가 많음)

public class Pizza {
    private String dough;      // 필수
    private String sauce;      // 필수
    private String cheese;     // 선택
    private String pepperoni;  // 선택
    private String mushroom;   // 선택
    private String onion;      // 선택
    
    // 생성자가 너무 많아진다
    public Pizza(String dough, String sauce) {
        this(dough, sauce, null);
    }
    
    public Pizza(String dough, String sauce, String cheese) {
        this(dough, sauce, cheese, null);
    }
    
    public Pizza(String dough, String sauce, String cheese, String pepperoni) {
        this(dough, sauce, cheese, pepperoni, null);
    }
    
    public Pizza(String dough, String sauce, String cheese, String pepperoni, String mushroom) {
        this(dough, sauce, cheese, pepperoni, mushroom, null);
    }
    
    public Pizza(String dough, String sauce, String cheese, String pepperoni, String mushroom, String onion) {
        this.dough = dough;
        this.sauce = sauce;
        this.cheese = cheese;
        this.pepperoni = pepperoni;
        this.mushroom = mushroom;
        this.onion = onion;
    }
}

// 사용할 때
Pizza pizza = new Pizza("씬", "토마토", "모짜렐라", null, "양송이", null);
// 문제: null이 뭘 의미하는지 알 수 없음!

 

문제점:

  1. 생성자가 너무 많아진다
  2. 매개변수 순서를 외워야 한다
  3. null이 무엇을 의미하는지 알 수 없다
  4. 실수하기 쉽다

방법 2: 자바빈즈 패턴 (문제가 있음)

public class Pizza {
    private String dough;
    private String sauce;
    private String cheese;
    private String pepperoni;
    private String mushroom;
    private String onion;
    
    // Setter 메서드들
    public void setDough(String dough) { this.dough = dough; }
    public void setSauce(String sauce) { this.sauce = sauce; }
    public void setCheese(String cheese) { this.cheese = cheese; }
    public void setPepperoni(String pepperoni) { this.pepperoni = pepperoni; }
    public void setMushroom(String mushroom) { this.mushroom = mushroom; }
    public void setOnion(String onion) { this.onion = onion; }
}

// 사용할 때
Pizza pizza = new Pizza();
pizza.setDough("씬");
pizza.setSauce("토마토");
pizza.setCheese("모짜렐라");
pizza.setMushroom("양송이");

문제점:

  1. 객체가 완전히 생성되기 전까지 일관성이 깨진다
  2. 불변 객체를 만들 수 없다 (setter가 있으니까)
  3. 스레드 안전하지 않다

빌더 패턴은 이 모든 문제를 해결한다!


순수 Java로 구현하기

기본 빌더 패턴

public class Pizza {
    // 필수 매개변수
    private final String dough;
    private final String sauce;
    
    // 선택 매개변수
    private final String cheese;
    private final String pepperoni;
    private final String mushroom;
    private final String onion;
    
    // private 생성자 - 직접 생성 불가
    private Pizza(Builder builder) {
        this.dough = builder.dough;
        this.sauce = builder.sauce;
        this.cheese = builder.cheese;
        this.pepperoni = builder.pepperoni;
        this.mushroom = builder.mushroom;
        this.onion = builder.onion;
    }
    
    // Builder 클래스 (내부 정적 클래스)
    public static class Builder {
        // 필수 매개변수
        private final String dough;
        private final String sauce;
        
        // 선택 매개변수 - 기본값으로 초기화
        private String cheese = "";
        private String pepperoni = "";
        private String mushroom = "";
        private String onion = "";
        
        // 필수 매개변수는 생성자로 받음
        public Builder(String dough, String sauce) {
            this.dough = dough;
            this.sauce = sauce;
        }
        
        // 선택 매개변수는 메서드로 설정 (메서드 체이닝)
        public Builder cheese(String cheese) {
            this.cheese = cheese;
            return this;
        }
        
        public Builder pepperoni(String pepperoni) {
            this.pepperoni = pepperoni;
            return this;
        }
        
        public Builder mushroom(String mushroom) {
            this.mushroom = mushroom;
            return this;
        }
        
        public Builder onion(String onion) {
            this.onion = onion;
            return this;
        }
        
        // 최종적으로 Pizza 객체 생성
        public Pizza build() {
            return new Pizza(this);
        }
    }
    
    @Override
    public String toString() {
        return "Pizza{" +
                "도우='" + dough + '\'' +
                ", 소스='" + sauce + '\'' +
                ", 치즈='" + cheese + '\'' +
                ", 페퍼로니='" + pepperoni + '\'' +
                ", 양송이='" + mushroom + '\'' +
                ", 양파='" + onion + '\'' +
                '}';
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        // 간단한 피자
        Pizza pizza1 = new Pizza.Builder("씬", "토마토")
                .cheese("모짜렐라")
                .build();
        
        System.out.println(pizza1);
        
        // 토핑 많은 피자
        Pizza pizza2 = new Pizza.Builder("오리지널", "크림")
                .cheese("체다")
                .pepperoni("페퍼로니")
                .mushroom("양송이")
                .onion("적양파")
                .build();
        
        System.out.println(pizza2);
    }
}

장점:

  • 코드가 읽기 쉽다 (무엇을 설정하는지 명확)
  • 원하는 옵션만 선택할 수 있다
  • 불변 객체가 생성된다
  • 매개변수 순서를 신경 쓸 필요 없다

실전 예제: 회원가입 폼

더 실무적인 예시를 보자.

public class User {
    // 필수
    private final String email;
    private final String password;
    private final String name;
    
    // 선택
    private final String nickname;
    private final int age;
    private final String phone;
    private final String address;
    private final boolean marketingAgree;
    
    private User(Builder builder) {
        this.email = builder.email;
        this.password = builder.password;
        this.name = builder.name;
        this.nickname = builder.nickname;
        this.age = builder.age;
        this.phone = builder.phone;
        this.address = builder.address;
        this.marketingAgree = builder.marketingAgree;
    }
    
    public static class Builder {
        // 필수
        private final String email;
        private final String password;
        private final String name;
        
        // 선택
        private String nickname = "";
        private int age = 0;
        private String phone = "";
        private String address = "";
        private boolean marketingAgree = false;
        
        public Builder(String email, String password, String name) {
            this.email = email;
            this.password = password;
            this.name = name;
        }
        
        public Builder nickname(String nickname) {
            this.nickname = nickname;
            return this;
        }
        
        public Builder age(int age) {
            this.age = age;
            return this;
        }
        
        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }
        
        public Builder address(String address) {
            this.address = address;
            return this;
        }
        
        public Builder marketingAgree(boolean agree) {
            this.marketingAgree = agree;
            return this;
        }
        
        public User build() {
            // 유효성 검증도 여기서 가능
            if (email == null || email.isEmpty()) {
                throw new IllegalArgumentException("이메일은 필수입니다");
            }
            if (password.length() < 8) {
                throw new IllegalArgumentException("비밀번호는 8자 이상이어야 합니다");
            }
            return new User(this);
        }
    }
    
    @Override
    public String toString() {
        return "User{" +
                "email='" + email + '\'' +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                ", age=" + age +
                ", phone='" + phone + '\'' +
                ", address='" + address + '\'' +
                ", marketingAgree=" + marketingAgree +
                '}';
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 최소 정보만 입력
        User user1 = new User.Builder("kim@email.com", "password123", "김도균")
                .build();
        
        System.out.println(user1);
        
        // 모든 정보 입력
        User user2 = new User.Builder("lee@email.com", "password456", "이철수")
                .nickname("철수")
                .age(25)
                .phone("010-1234-5678")
                .address("서울시 강남구")
                .marketingAgree(true)
                .build();
        
        System.out.println(user2);
    }
}

Lombok을 사용한 간편한 빌더

실무에서는 Lombok의 @Builder 어노테이션을 많이 사용한다.

import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class Coffee {
    private String size;        // 사이즈
    private String type;        // 종류
    private boolean iceOrHot;   // 아이스/핫
    private int shot;           // 샷 개수
    private boolean whipping;   // 휘핑크림
    private boolean syrup;      // 시럽
}

// 사용
public class Main {
    public static void main(String[] args) {
        Coffee coffee = Coffee.builder()
                .size("Tall")
                .type("아메리카노")
                .iceOrHot(true)
                .shot(2)
                .build();
        
        System.out.println(coffee);
    }
}

 

엄청 간단해진다! 하지만 필수 값 검증 같은 커스텀 로직이 필요하면 직접 구현하는 게 낫다.


빌더 패턴의 장점

  1. 가독성: 코드가 읽기 쉽고 명확하다
  2. 유연성: 필요한 값만 설정할 수 있다
  3. 불변성: 불변 객체를 쉽게 만들 수 있다
  4. 안전성: 잘못된 객체 생성을 방지할 수 있다
  5. 확장성: 새로운 옵션 추가가 쉽다

빌더 패턴의 단점

  1. 코드량 증가: Builder 클래스를 추가로 작성해야 함
  2. 성능: 객체를 하나 더 만들어야 함 (큰 문제는 아님)
  3. 단순한 객체: 매개변수가 4개 이하면 오히려 복잡할 수 있음

Spring에서의 빌더 패턴

Spring에서도 빌더 패턴을 자주 사용한다.

// DTO에 빌더 사용
@Builder
public class UserCreateRequest {
    private String email;
    private String password;
    private String name;
    private String nickname;
}

// 엔티티에 빌더 사용
@Entity
@Builder
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    private String content;
    private String author;
}

// 서비스에서 사용
@Service
public class PostService {
    public Post createPost(PostCreateRequest request) {
        return Post.builder()
                .title(request.getTitle())
                .content(request.getContent())
                .author(request.getAuthor())
                .build();
    }
}

실무에서 언제 사용할까?

사용하면 좋은 경우:

  • 매개변수가 많을 때 (4개 이상)
  • 선택적 매개변수가 많을 때
  • 불변 객체를 만들어야 할 때
  • 객체 생성 과정이 복잡할 때
  • 유효성 검증이 필요할 때

사용하지 않아도 되는 경우:

  • 매개변수가 적을 때 (2-3개)
  • 모든 매개변수가 필수일 때
  • 객체가 자주 변경될 때 (가변 객체)

핵심 원칙:

"복잡한 객체를 만들 땐 빌더를, 간단한 객체는 생성자를 사용하라"


생성자 vs 빌더 비교

// 생성자 방식 - 간단할 때
Point point = new Point(10, 20);

// 빌더 방식 - 복잡할 때
Pizza pizza = new Pizza.Builder("씬", "토마토")
        .cheese("모짜렐라")
        .pepperoni("페퍼로니")
        .mushroom("양송이")
        .build();

결론

빌더 패턴은 복잡한 객체를 가독성 좋게 생성하는 강력한 패턴이다.

기억해야 할 포인트:

  • 메서드 체이닝으로 가독성이 높아진다
  • 필수 값과 선택 값을 명확히 구분한다
  • 불변 객체 생성에 최적이다
  • Lombok의 @Builder로 간편하게 사용 가능하다
  • 매개변수가 많거나 복잡할 때만 사용한다

다음 글에서는 기존 객체를 복사해서 새로운 객체를 만드는 프로토타입 패턴을 알아볼 것이다.

다음 글 예고: 프로토타입 패턴 - 객체를 복사해서 새로 만들어라