최근 LLM 파이프라인 공부를 하면서 다양한 코드를 살펴보고 있습니다. 그 과정에서 많은 디자인 패턴들을 접하게 되는데 이번에 builder pattern(빌더 패턴)에 대해 처음 접하게 되어 제가 이해한 빌더 패턴을 저의 언어로 풀어서 작성해보았습니다.
디자인 패턴에 대한 지식이 부족하여 잘못 이해하고 있는 부분이 있을 수 있어 그런 점들은 피드백 해주시면 감사드리겠습니다.
빌더 패턴이 필요한 이유
빌더 패턴이 생겨난 가장 큰 이유는 객체 생성과 객체 사용을 분리하여 코드의 안정성과 유지관리성을 높이기 위해서 입니다. 예를 들어 Pizza이라는 객체가 있다고 해보겠습니다.
// Pizza 클래스 정의
public class Pizza {
// 필수 속성
private String dough; // 반죽
private String sauce; // 소스
// 선택 속성
private String cheese; // 치즈
private String topping; // 토핑
// 생성자: 필수 속성만 초기화
public Pizza(String dough, String sauce) {
this.dough = dough;
this.sauce = sauce;
}
// getter 및 setter 메서드
public String getDough() {
return dough;
}
public void setDough(String dough) {
this.dough = dough;
}
public String getSauce() {
return sauce;
}
public void setSauce(String sauce) {
this.sauce = sauce;
}
public String getCheese() {
return cheese;
}
public void setCheese(String cheese) {
this.cheese = cheese;
}
public String getTopping() {
return topping;
}
public void setTopping(String topping) {
this.topping = topping;
}
// Pizza의 정보 출력
@Override
public String toString() {
return "Pizza { " +
"dough='" + dough + '\'' +
", sauce='" + sauce + '\'' +
", cheese='" + cheese + '\'' +
", topping='" + topping + '\'' +
" }";
}
}
Java
복사
피자를 만들기 위해 도우, 소스, 치즈, 토핑 이렇게 4가지 재료가 필요하다고 했을 때 도우와 소스는 필수 재료이기 때문에 생성자에서 정의하고 나머지 재료는 setter 메소드를 통해 정의하도록 만들 수 있을겁니다. 그래서 아래와 같은 방법으로 다양한 피자 객체를 생성할 수 있죠.
public class Main {
public static void main(String[] args) {
// 마르게리따 피자
Pizza margheritaPizza = new Pizza("Thin Crust", "Tomato Sauce");
margheritaPizza.setCheese("Mozzarella"); // 선택적으로 치즈 추가
System.out.println(margheritaPizza);
// 커스텀 피자 생성
Pizza customPizza = new Pizza("Thick Crust", "Barbecue Sauce");
customPizza.setCheese("Cheddar");
customPizza.setTopping("Pepperoni");
System.out.println(customPizza);
}
}
Java
복사
하지만 이는 2가지 문제가 있는데요.
1.
객체 생성과 속성 설정이 분리되어 있어 객체의 일관성 문제가 발생할 수 있습니다.
2.
객체 생성 이후에도 setter 메소드로 객체의 속성값을 변경할 수 있는 불변성 문제가 발생할 수 있습니다.
빌더 패턴으로 리팩터링
위의 코드에 빌더 패턴을 적용해보도록 하겠습니다. 리팩터링한 Pizza 클래스는 이제 Builder라는 내부 정적 클래스를 통해 구성할 수 있도록 설계되었습니다. 이를 통해 피자를 완성하기 위해서는 클래스의 빌더를 호출한 뒤 단계별로 속성을 체이닝 형태로 호출하게 됩니다.
// Pizza 클래스 정의
public class Pizza {
// 필수 속성
private final String dough; // 반죽
private final String sauce; // 소스
// 선택 속성
private final String cheese; // 치즈
private final String topping; // 토핑
// private 생성자: Builder가 호출하도록 설정
private Pizza(Builder builder) {
this.dough = builder.dough;
this.sauce = builder.sauce;
this.cheese = builder.cheese;
this.topping = builder.topping;
}
// Pizza의 정보 출력 메서드
@Override
public String toString() {
return "Pizza { " +
"dough='" + dough + '\'' +
", sauce='" + sauce + '\'' +
", cheese='" + cheese + '\'' +
", topping='" + topping + '\'' +
" }";
}
// Builder 클래스 정의
public static class Builder {
// 필수 속성 초기화
private final String dough;
private final String sauce;
// 선택 속성 초기화
private String cheese = ""; // 기본값
private String topping = ""; // 기본값
// Builder 생성자: 필수 속성 설정
public Builder(String dough, String sauce) {
this.dough = dough;
this.sauce = sauce;
}
// 선택 속성인 cheese 설정 메서드
public Builder cheese(String cheese) {
this.cheese = cheese;
return this; // Builder 객체를 반환
}
// 선택 속성인 topping 설정 메서드
public Builder topping(String topping) {
this.topping = topping;
return this; // Builder 객체를 반환
}
// 최종 Pizza 객체 생성 메서드
public Pizza build() {
return new Pizza(this);
}
}
}
Java
복사
하나의 피자를 만들기 위해서 Pizza내의 Builder 클래스를 통해 필요한 재료들을 메서드 형태로 체이닝 하여 호출하고, build()를 통해 최종적으로 원하는 형태의 Pizza 객체를 생성하는 것을 볼 수 있습니다.
public class Main {
public static void main(String[] args) {
// 마르게리타 피자 생성
Pizza margheritaPizza = new Pizza.Builder("Thin Crust", "Tomato Sauce")
.cheese("Mozzarella")
.build(); // 치즈만 선택적으로 추가
System.out.println(margheritaPizza);
// 커스텀 피자 생성
Pizza customPizza = new Pizza.Builder("Thick Crust", "Barbecue Sauce")
.cheese("Cheddar")
.topping("Pepperoni")
.build();
System.out.println(customPizza);
}
}
Java
복사
빌더 패턴을 적용함으로써 가장 큰 장점은 객체의 생성 과정을 쉽게 파악할 수 있다는 점입니다. 만약 생성자로 이를 처리했다면 매개변수가 많아지면 순서를 파악하기 어려워 가독성 측면에서 좋지 않았을 것입니다. 또한 디폴트 매개변수를 설정할 수 있다는 것도 하나의 장점이 될 수 있습니다. 다만 클래스 생성마다 빌드 클래스를 생성해주어야 하는 복잡성과 객체의 생성비용 증가로 인한 성능 하락이 있을 수 있습니다.
개인적으로 빌더 패턴은 자바에서는 여러 장점을 가지는 것으로 보이긴 하지만 파이썬에서는 굳이? 라는 생각이 들었습니다. 빌더 패턴의 가장 큰 사용이유는 파라미터 관리라고 생각하는데 파이썬에서는 keyword parameter가 있기 때문에 가독성이나 안정성이 어느정도 보장되어 있어서 오히려 코드의 복잡성을 올리는 패턴이 될 수도 있겠다는 생각이 들기도 하였습니다.