Builder Pattern 정리

테사벨로그·2025년 10월 31일

Design Pattern

목록 보기
15/19

1. 왜 Builder Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 복잡한 생성자와 불명확한 객체 생성
public class Airplane {
    public Airplane(float wingspan, String powerplant, String avionics, 
                   int crewSeats, int passengerSeats, String type, String customer) {
        // 매개변수가 너무 많고 순서를 기억하기 어려움
        // 일부는 필수, 일부는 선택적일 수 있음
    }
}

// 클라이언트 코드
Airplane cropDuster = new Airplane(9f, "single piston", null, 1, 1, "Crop Duster", "Joe");
Airplane glider = new Airplane(57.1f, null, null, 1, 0, "Glider", "Tim");  // null이 많아짐

문제점:

  • 생성자 매개변수가 너무 많아서 순서를 기억하기 어려움
  • 선택적 매개변수 처리가 불편 (null 사용)
  • 다양한 표현(representation)의 객체를 만들기 어려움
  • 객체 생성 로직이 변경되면 모든 클라이언트 코드 수정 필요
  • 생성 과정이 복잡한 경우 단계별 제어 불가능

해결책:

  • 동적으로 객체를 생성하자.

2. Director VS Builder Interface VS Concrete Builder

1. Director (지휘자)

  • "무엇을(what) 만들어야 하는지" 알고 있음
  • 객체 생성에 어떤 단계가 필요한지 정의
  • "has-a" 관계 (Director는 Builder를 가짐)
public class AerospaceEngineer {  // Director
    private AirplaneBuilder builder;
    
    public void constructAirplane() {
        builder.createNewAirplane();
        builder.buildWings();        // 1단계
        builder.buildPowerplant();   // 2단계
        builder.buildAvionics();     // 3단계
        builder.buildSeats();        // 4단계
    }
}

왜 필요한가?

  • 복잡한 객체의 생성 순서와 단계를 캡슐화
  • 다양한 Builder와 함께 작동 (Builder만 교체하면 됨)

2. Builder Interface (추상 빌더)

  • "어떻게(how) 만드는지"의 규격만 정의
  • 각 부품을 만드는 메서드의 시그니처만 제공
  • "can-do" 관계 (Builder는 부품을 만들 수 있음)
public abstract class AirplaneBuilder {
    protected Airplane airplane;
    
    public abstract void buildWings();
    public abstract void buildPowerplant();
    public abstract void buildAvionics();
    public abstract void buildSeats();
    
    public Airplane getAirplane() { return airplane; }
}

왜 Abstract Class/Interface인가?

  • 다양한 구체적 Builder가 동일한 인터페이스 구현
  • Director는 구체적인 Builder를 몰라도 됨 (느슨한 결합)

3. Concrete Builder (구체적 빌더)

  • 실제로 "어떻게(how)" 각 부품을 만들고 조립하는지 구현
  • 생성 중인 객체의 표현(representation)을 관리
  • 최종 Product를 반환하는 메서드 제공
public class CropDuster extends AirplaneBuilder {
    public void buildWings() {
        airplane.setWingspan(9f);  // 구체적인 구현
    }
    public void buildPowerplant() {
        airplane.setPowerplant("single piston");
    }
    // ... 나머지 메서드 구현
}

3. 왜 Interface/Abstract Class를 사용하는가?

Builder가 Abstract Class인 이유

  1. 공통 코드 재사용

    public abstract class AirplaneBuilder {
        protected Airplane airplane;  // 모든 Builder가 공유하는 필드
        
        // 공통 메서드 (모든 Builder가 같은 방식으로 사용)
        public void createNewAirplane() {
            airplane = new Airplane(customer, type);
        }
        
        public Airplane getAirplane() { 
            return airplane; 
        }
    }
  2. 부분적 구현 강제

    • 일부 메서드는 구현(createNewAirplane, getAirplane)
    • 일부 메서드는 강제로 override (buildWings, buildPowerplant 등)
  3. 상속 계층 구조

    • 모든 Concrete Builder가 동일한 타입으로 취급됨
    • Director는 AirplaneBuilder 타입만 알면 됨

Interface 대신 Abstract Class를 사용하는 경우

  • 공유할 상태(필드)가 있을 때: protected Airplane airplane
  • 일부 공통 메서드를 제공하고 싶을 때
  • 단일 상속으로 충분할 때

4. Builder Pattern 핵심 구조


   Client
      |
      | 1. Concrete Builder 생성
      | 2. Director에 Builder 전달
      ↓
   Director (무엇을 만들지)
      |
      | build() 호출
      ↓
   Builder (어떻게 만드는지의 규격)
      ↑
      |
   ConcreteBuilder1, ConcreteBuilder2, ...
   (실제 구현 - 다양한 표현)
      |
      ↓
   Product (복잡한 객체)

핵심 흐름:
1. Client가 Concrete Builder 생성
2. Client가 Director를 생성하고 Builder를 주입
3. Client가 Director.build() 호출
4. Director가 Builder의 메서드를 순서대로 호출
5. Builder가 단계별로 Product 조립
6. Client가 Builder에서 완성된 Product 획득


5. 예시 코드

Step 1: Product (복잡한 객체)

// Product: 만들어질 복잡한 객체
public class Airplane {
    private String type;
    private float wingspan;
    private String powerplant;
    private int crewSeats;
    private int passengerSeats;
    private String avionics;
    private String customer;
    
    public Airplane(String customer, String type) {
        this.customer = customer;
        this.type = type;
    }
    
    // Setters
    public void setWingspan(float w) { this.wingspan = w; }
    public void setPowerplant(String p) { this.powerplant = p; }
    public void setAvionics(String a) { this.avionics = a; }
    public void setNumberSeats(int crew, int passenger) {
        this.crewSeats = crew;
        this.passengerSeats = passenger;
    }
    
    // Getters
    public String getCustomer() { return customer; }
    public String getType() { return type; }
}

Step 2: Abstract Builder

// Builder: 추상 빌더
public abstract class AirplaneBuilder {
    protected Airplane airplane;
    protected String customer;
    protected String type;
    
    // 공통 메서드
    public Airplane getAirplane() {
        return airplane;
    }
    
    public void createNewAirplane() {
        airplane = new Airplane(customer, type);
    }
    
    // 각 Concrete Builder가 구현해야 할 메서드
    public abstract void buildWings();
    public abstract void buildPowerplant();
    public abstract void buildAvionics();
    public abstract void buildSeats();
}

Step 3: Concrete Builder 구현

// Concrete Builder 1: 농업용 비행기
public class CropDuster extends AirplaneBuilder {
    public CropDuster(String customer) {
        super.customer = customer;
        super.type = "Crop Duster v3.4";
    }
    
    public void buildWings() {
        airplane.setWingspan(9f);
    }
    
    public void buildPowerplant() {
        airplane.setPowerplant("single piston");
    }
    
    public void buildAvionics() {
        // 농업용은 항공전자장비 불필요
    }
    
    public void buildSeats() {
        airplane.setNumberSeats(1, 1);
    }
}

// Concrete Builder 2: 전투기
public class FighterJet extends AirplaneBuilder {
    public FighterJet(String customer) {
        super.customer = customer;
        super.type = "F-35 Lightning II";
    }
    
    public void buildWings() {
        airplane.setWingspan(35.0f);
    }
    
    public void buildPowerplant() {
        airplane.setPowerplant("dual thrust vectoring");
    }
    
    public void buildAvionics() {
        airplane.setAvionics("military");
    }
    
    public void buildSeats() {
        airplane.setNumberSeats(1, 0);  // 조종사만
    }
}

// Concrete Builder 3: 글라이더
public class Glider extends AirplaneBuilder {
    public Glider(String customer) {
        super.customer = customer;
        super.type = "Glider v9.0";
    }
    
    public void buildWings() {
        airplane.setWingspan(57.1f);  // 긴 날개
    }
    
    public void buildPowerplant() {
        // 글라이더는 엔진 없음
    }
    
    public void buildAvionics() {
        // 기본 장비만
    }
    
    public void buildSeats() {
        airplane.setNumberSeats(1, 0);
    }
}

Step 4: Director 구현

// Director: 생성 과정을 제어
public class AerospaceEngineer {
    private AirplaneBuilder airplaneBuilder;
    
    // Builder 설정
    public void setAirplaneBuilder(AirplaneBuilder ab) {
        airplaneBuilder = ab;
    }
    
    // 완성된 비행기 반환
    public Airplane getAirplane() {
        return airplaneBuilder.getAirplane();
    }
    
    // 비행기 생성 과정 (순서가 중요!)
    public void constructAirplane() {
        airplaneBuilder.createNewAirplane();
        airplaneBuilder.buildWings();       // 1단계
        airplaneBuilder.buildPowerplant();  // 2단계
        airplaneBuilder.buildAvionics();    // 3단계
        airplaneBuilder.buildSeats();       // 4단계
    }
}

Step 5: Client 코드

public class BuilderExample {
    public static void main(String[] args) {
        // 1. Director 생성 (엔지니어 고용)
        AerospaceEngineer engineer = new AerospaceEngineer();
        
        // 2. Concrete Builder들 생성 (주문 접수)
        AirplaneBuilder cropDusterBuilder = new CropDuster("Farmer Joe");
        AirplaneBuilder fighterJetBuilder = new FighterJet("The Navy");
        AirplaneBuilder gliderBuilder = new Glider("Tim Rice");
        
        // 3. 농업용 비행기 제작
        engineer.setAirplaneBuilder(cropDusterBuilder);
        engineer.constructAirplane();
        Airplane cropDuster = engineer.getAirplane();
        System.out.println(cropDuster.getType() + 
            " 완성 → 고객: " + cropDuster.getCustomer());
        
        // 4. 전투기 제작 (같은 Director, 다른 Builder)
        engineer.setAirplaneBuilder(fighterJetBuilder);
        engineer.constructAirplane();
        Airplane fighter = engineer.getAirplane();
        System.out.println(fighter.getType() + 
            " 완성 → 고객: " + fighter.getCustomer());
        
        // 5. 글라이더 제작
        engineer.setAirplaneBuilder(gliderBuilder);
        engineer.constructAirplane();
        Airplane glider = engineer.getAirplane();
        System.out.println(glider.getType() + 
            " 완성 → 고객: " + glider.getCustomer());
    }
}

출력 결과

Crop Duster v3.4 완성 → 고객: Farmer Joe
F-35 Lightning II 완성 → 고객: The Navy
Glider v9.0 완성 → 고객: Tim Rice

6. 실전 예제: 문서 변환기

// Product
public class Document {
    private StringBuilder content = new StringBuilder();
    
    public void addParagraph(String text) {
        content.append(text);
    }
    
    public String getContent() {
        return content.toString();
    }
}

// Abstract Builder
public abstract class DocumentBuilder {
    protected Document document = new Document();
    
    public abstract void addHeader(String text);
    public abstract void addParagraph(String text);
    public abstract void addFooter(String text);
    
    public Document getDocument() {
        return document;
    }
}

// Concrete Builder 1: HTML
public class HTMLBuilder extends DocumentBuilder {
    public void addHeader(String text) {
        document.addParagraph("<h1>" + text + "</h1>\n");
    }
    
    public void addParagraph(String text) {
        document.addParagraph("<p>" + text + "</p>\n");
    }
    
    public void addFooter(String text) {
        document.addParagraph("<footer>" + text + "</footer>\n");
    }
}

// Concrete Builder 2: Markdown
public class MarkdownBuilder extends DocumentBuilder {
    public void addHeader(String text) {
        document.addParagraph("# " + text + "\n\n");
    }
    
    public void addParagraph(String text) {
        document.addParagraph(text + "\n\n");
    }
    
    public void addFooter(String text) {
        document.addParagraph("---\n" + text + "\n");
    }
}

// Director
public class DocumentDirector {
    private DocumentBuilder builder;
    
    public void setBuilder(DocumentBuilder b) {
        this.builder = b;
    }
    
    public void constructDocument() {
        builder.addHeader("제목");
        builder.addParagraph("본문 내용 1");
        builder.addParagraph("본문 내용 2");
        builder.addFooter("© 2024");
    }
    
    public Document getDocument() {
        return builder.getDocument();
    }
}

// 사용
DocumentDirector director = new DocumentDirector();

// HTML 문서 생성
director.setBuilder(new HTMLBuilder());
director.constructDocument();
Document htmlDoc = director.getDocument();

// Markdown 문서 생성 (같은 순서, 다른 형식)
director.setBuilder(new MarkdownBuilder());
director.constructDocument();
Document mdDoc = director.getDocument();

7. 핵심 정리

Builder Pattern의 구성

요소역할특징
Director생성 순서 정의 (what)Builder를 사용해 단계별 생성 과정 제어
Builder (Abstract)생성 인터페이스 정의각 부품을 만드는 메서드 시그니처 제공
Concrete Builder실제 생성 구현 (how)구체적인 표현 방식으로 객체 조립
Product최종 결과물복잡한 구조를 가진 객체

언제 사용하는가?

  • 복잡한 객체를 단계별로 생성해야 할 때
  • 동일한 생성 과정으로 다양한 표현을 만들어야 할 때
  • ✅ 객체 생성 알고리즘을 분리하고 싶을 때
  • 런타임에 생성 과정을 제어해야 할 때
  • ✅ 생성자 매개변수가 많거나 선택적 매개변수가 많을 때

핵심 원칙

  • 관심사의 분리: 생성 과정(Director) vs 표현 방식(Builder)
  • 단계별 생성: 복잡한 객체를 한 번에 만들지 않고 단계별로 조립
  • 다형성 활용: 같은 Director로 다양한 Builder 사용 가능
  • 유연성: 새로운 Builder 추가 시 기존 코드 수정 불필요

Abstract Factory와 비교

BuilderAbstract Factory
단계별 객체 생성즉시 완성된 객체 반환
Director가 생성 과정 제어Client가 직접 Factory 메서드 호출
나중에 getProduct()로 결과 획득메서드 호출 즉시 객체 반환
복잡한 객체의 조립 과정에 집중관련된 객체 패밀리 생성에 집중

주의사항

  • 인터페이스가 불안정하면 사용하지 말 것

    • Builder 인터페이스 변경 시 모든 Concrete Builder 수정 필요
    • 새 메서드 추가 시 모든 구현체가 영향받음
  • ❌ 단순한 객체에는 과도한 설계

    • 생성 과정이 단순하면 일반 생성자나 Factory로 충분

8. 관련 패턴

  • Composite: Builder가 자주 생성하는 결과물
  • Strategy: Builder는 복잡한 객체 생성에 특화된 Strategy
  • Abstract Factory: 둘 다 객체 생성 패턴이지만 접근 방식이 다름
  • Prototype: Builder 대신 복제로 복잡한 객체 생성 가능
profile
다들 응원합니다.

0개의 댓글