Builder
Builder는 복잡한 객체의 생성 과정을 단계별로 분리해서, 같은 생성 절차가 서로 다른 표현의 객체를 만들 수 있게 해주는 생성 패턴입니다. 생성자(constructor)에 매개변수를 한꺼번에 몰아넣는 대신, 필요한 부분을 하나씩 설정하고 마지막에 완성된 객체를 받습니다.
▶아키텍처 다이어그램
🔄 프로세스 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
생성자 매개변수가 10개 넘는 클래스를 본 적이 있을 겁니다. 어떤 값이 어떤 필드에 들어가는지 순서를 외워야 하고, 선택적 매개변수는 null을 넣어야 합니다. `new House(4, 2, true, false, null, null, true, 'garage', null, 'pool')` 같은 호출은 읽는 사람이 각 인자의 의미를 파악하기 어렵습니다. 여기에 표현이 여러 가지로 갈리면 문제가 커집니다. 같은 집이라도 나무집, 돌집, 유리집의 자재와 공정이 다르고, 이런 변형을 생성자 오버로드나 조건문으로 처리하면 생성 로직과 표현 로직이 뒤엉킵니다. 수정할 때마다 다른 변형에 영향이 갈까 걱정해야 합니다.
소프트웨어가 복잡해지면서 하나의 객체가 가지는 구성 요소가 급격히 늘어났습니다. 데이터베이스 커넥션 풀, HTTP 클라이언트, 문서 파서처럼 설정 항목이 수십 개인 객체가 일상이 됐습니다. 초기에는 생성자에 매개변수를 추가하거나, setter를 열어 두거나, 여러 생성자를 오버로드하는 방식으로 버텨 왔습니다. 하지만 이 접근법은 매개변수가 늘어날수록 유지보수가 어려워졌고, setter를 열면 객체가 불완전한 중간 상태로 외부에 노출되는 문제가 생겼습니다. Builder 패턴은 이 압력 속에서 '생성 과정을 단계로 쪼개되, 완성될 때까지 외부에 노출하지 않는' 방법으로 정리됐습니다.
Builder 패턴은 네 가지 역할로 구성됩니다. Builder 인터페이스가 buildWalls(), buildRoof(), buildGarage() 같은 생성 단계를 선언합니다. ConcreteBuilder가 이 단계들을 실제로 구현하며, 내부에 중간 상태를 쌓아 갑니다. Director는 빌드 단계의 호출 순서를 정의합니다. '벽을 먼저 세우고, 지붕을 얹고, 차고를 붙인다'는 절차를 캡슐화합니다. 핵심은 메서드 호출 순서가 곧 조립 과정이라는 점입니다. 각 단계에서 Builder는 자기 자신(this)을 반환해 체이닝할 수도 있고, Director가 순서를 관리할 수도 있습니다. 마지막에 getResult()를 호출하면 조립이 끝난 완성 객체를 받습니다. 같은 Director에 다른 ConcreteBuilder를 넣으면 같은 절차로 다른 표현의 결과물이 나옵니다.
Builder 인터페이스와 구현
interface HouseBuilder {
buildWalls(material: string): this;
buildRoof(type: string): this;
buildGarage(count: number): this;
getResult(): House;
}
class ConcreteHouseBuilder implements HouseBuilder {
private house = new House();
buildWalls(material: string) {
this.house.walls = material;
return this;
}
buildRoof(type: string) {
this.house.roof = type;
return this;
}
buildGarage(count: number) {
this.house.garages = count;
return this;
}
getResult() {
return this.house;
}
}각 메서드가 this를 반환하므로 체이닝이 가능합니다. 중간 상태는 Builder 내부에 머물고, getResult()를 호출해야 완성 객체가 외부로 나갑니다.
Director와 클라이언트
class HouseDirector {
constructLuxuryHouse(builder: HouseBuilder) {
builder
.buildWalls('stone')
.buildRoof('tile')
.buildGarage(2);
}
}
const builder = new ConcreteHouseBuilder();
const director = new HouseDirector();
director.constructLuxuryHouse(builder);
const house = builder.getResult();Director는 빌드 순서만 알고, 구체적인 구현은 모릅니다. Director 없이 클라이언트가 직접 단계를 호출해도 됩니다.
Builder와 Abstract Factory는 둘 다 복잡한 객체 생성을 다루지만 초점이 다릅니다. Abstract Factory는 '어떤 제품군을 만들 것인가'를 결정하고 관련 객체를 한꺼번에 생성합니다. Builder는 '하나의 복잡한 객체를 어떻게 조립할 것인가'를 단계로 분해합니다. 생성자 텔레스코핑(telescoping constructor) 문제만 해결하려 한다면, 언어에 따라 named parameter나 옵션 객체 패턴으로도 충분할 수 있습니다. Builder가 진짜 필요한 순간은 생성 과정 자체에 순서가 있거나, 같은 과정으로 다른 표현을 만들어야 하거나, 중간 상태를 외부에 노출하면 안 되는 경우입니다. Builder의 단점은 클래스 수가 늘어난다는 점입니다. 단순한 객체에 Builder를 도입하면 오히려 코드가 복잡해집니다. 생성자 매개변수가 서넛 이하이고 변형도 없다면 Builder 없이 직접 생성하는 편이 낫습니다.
Builder 패턴은 설정 항목이 많은 객체를 다룰 때 자주 나타납니다. SQL 쿼리 빌더가 대표적입니다. `query.select('name').from('users').where('age > 20').orderBy('name')` 같은 체이닝이 Builder 패턴의 전형적인 활용입니다. HTTP 클라이언트 라이브러리에서도 Request 객체를 빌더로 구성하는 경우가 많습니다. URL, 헤더, 바디, 인증, 타임아웃 같은 설정을 단계별로 쌓고 마지막에 build()를 호출합니다. Builder를 고려할 신호는 '생성자 매개변수 순서를 외워야 하거나, 선택적 매개변수에 null을 넣는 코드가 반복되거나, 같은 구조의 객체를 여러 변형으로 만들어야 하는' 상황입니다. 반대로, 객체가 단순하고 필수 필드만 있다면 생성자 하나로 충분합니다.