2026년 2월
일회용 Method 빌더패턴
Closure와 omit으로 빌드타임에 method 중복 호출을 막는 방법
빌더 패턴 다시 생각하기
Builder 패턴은 복잡한 객체를 단계적으로 조립할 때 자주 씁니다. 햄버거를 예로 들면, 번·패티·치즈·야채·소스를 하나씩 설정한 뒤
build()return this// setBun을 두 번 호출해도 타입 오류 없음 — 마지막 값만 남음
const overwritten = new HamburgerBuilder()
.setBun("sesame")
.setBun("brioche")
.build();
overwritten.describe();
// Bun: brioche
// ...전통적인 Builder 패턴에서
setBun()setPatty()setSauce()저는 이런 구조가 불안하다고 느꼈습니다. 잘못 사용하면 builder가 여러 함수를 넘어다니면서 side effect를 일으킬 수 있어 보였습니다. 번·패티· 소스를 1번만 선택하게 강제할 수 있을까요? 강제할 수 있다면 빌드타임에 막을 수 있을까요?
아이디어 1 — Builder를 두 요소로 나누기
문제를 해결하기 위해서 떠오른 첫번째 생각은 'method를 사용할 때마다, builder의 정의가 새롭게 작성되도록 만드면 되겠다.' 였습니다. 사용한 method를 뺀 새로운 builder를 다시 만들어 사용하면 될 것 같았습니다.
그래서 일단 builder를 data 객체와 method 객체로 나누어 보았습니다. 왜냐하면 data 부분은 변경점이 없고, method 부분만 재정의 되어야하기 때문에, 관심사를 분리하기 위해서입니다.
type Data = {
bun: string;
patty: string;
cheese: boolean;
vegetables: string[];
sauce: string;
};
const _data = {
bun: "plain",
patty: "beef",
cheese: false,
vegetables: [],
sauce: "none",
};이제 method 객체를 함수로 감싸서, method를 호출할 때마다 method 객체를 새로 만들어 만들어 반환하도록 합니다. 이제 method를 사용할 때마다, builder의 type을 바꿀 수 있는 기반이 마련되었습니다.
추가적으로 class 형식의 builder 패턴과 같이, method와 같이 data가 흐르도록 파라미터를 수정합니다.
function createBuilder() {
const method = {
setBun: (bun: string) => {
_data.bun = bun;
return createBuilder();
},
setPatty: (patty: string) => {
_data.patty = patty;
return createBuilder();
},
addCheese: () => {
_data.cheese = true;
return createBuilder();
},
addVegetable(vege: string) {
_data.vegetables.push(vege);
return createBuilder();
},
setSauce(sauce: string) {
_data.sauce = sauce;
return createBuilder();
},
build() { ... },
};
return method;
}아이디어 2 — 호출된 method 제거하기
한번 호출된 method는 계속 호출할 수 없어야합니다. 이를 위해서는 어떤 method가 제거되었는지 정보가 필요합니다. createBuilder 함수에
omitsfunction createBuilder<O extends keyof typeof builder = never>(
data: Data = _data,
omits: O[] = [],
) {
const method = {
setBun: (bun: string) => {
return createBuilder({ ...data, bun }, [...omits, "setBun"]);
},
...,
addCheese: () => {
return createBuilder({ ...data, cheese: true }, omits);
},
...,
build() { ... },
};
return omit(method, omits);
}setBun()omits반면에
addCheese()omits타입 추론과 method 가용성
체인이 진행될수록
omitsconst builder = createBuilder();
// 사용 가능: setBun, setPatty, addCheese, addVegetable, setSauce, buildClass builder는 가능하지만 비추천
Closure 없이 class로도 비슷한 제약을 줄 수 있습니다. 관례대로
return thisreturn this as ...asOtype OnceMethod =
| "setBun"
| "setPatty"
| "addCheese"
| "addVegetable"
| "setSauce"
| "build";
class OnceHamburgerBuilder<O extends OnceMethod = never> {
private bun = "plain";
private patty = "beef";
private cheese = false;
private vegetables: string[] = [];
private sauce = "none";
setBun(bun: string) {
this.bun = bun;
return this as Omit<
OnceHamburgerBuilder<O | "setBun">,
"setBun"
>;
}
setPatty(patty: string) {
this.patty = patty;
return this as Omit<
OnceHamburgerBuilder<O | "setPatty">,
"setPatty"
>;
}
addCheese() {
this.cheese = true;
return this as OnceHamburgerBuilder<O>;
}
addVegetable(vege: string) {
this.vegetables.push(vege);
return this as OnceHamburgerBuilder<O>;
}
setSauce(sauce: string) {
this.sauce = sauce;
return this as Omit<
OnceHamburgerBuilder<O | "setSauce">,
"setSauce"
>;
}
build() {
return new Hamburger(
this.bun,
this.patty,
this.cheese,
this.vegetables,
this.sauce,
);
}
}한계도 명확합니다.
asClosure +
omit한계점
createBuilder 정의부 타입이 매우 복잡합니다. IDE에서 hover하면 중첩된
Omit<...>function createBuilder<
O extends
| "setBun"
| "setPatty"
| "addCheese"
| "addVegetable"
| "setSauce"
| "build" = never,
>(
data?: Data,
omits?: O[],
): Omit<{
setBun: (bun: string) => Omit<{
setBun: (bun: string) => Omit<..., O | "setBun">;
setPatty: (patty: string) => Omit<{
setBun: (bun: string) => Omit<..., O | "setBun" | "setPatty">;
setPatty: (patty: string) => Omit<..., O | "setBun" | "setPatty">;
addCheese: () => Omit<..., O | "setBun" | "setPatty">;
addVegetable(vege: string): Omit<..., O | "setBun" | "setPatty">;
setSauce(sauce: string): Omit<...>;
build(): Hamburger;
}, O | ... 1 more ... | "setPatty">;
addCheese: () => Omit<...>;
addVegetable(vege: string): Omit<...>;
setSauce(sauce: string): Omit<...>;
build(): Hamburger;
}, O | "setBun">;
... 4 more ...;
build(): Hamburger;
}, O>메모리 overhead가 있습니다. Class builder는 하나의 reference를 유지하지만, closure builder는 method 호출마다 새 객체를 생성합니다. 비슷한 객체를 쉽게 만들기 위한 가벼운 wrapper를 제공하는 것이 builder의 본래 목적이라고 한다면, 이 방식은 메모리 관점에서 불리할 수 있습니다.