1. 기본 구조: Pizza 클래스
Java에서는 객체 생성과 관련된 복잡한 요구 사항을 처리할 때 빌더 패턴(Builder Pattern)을 자주 사용합니다. 특히 Effective Java에서는 객체 생성 시 인자가 많거나 선택적인 경우 빌더 패턴을 통해 객체를 명확하고 안전하게 생성할 수 있다고 강조합니다. 오늘은 Pizza 클래스를 예시로 하여 빌더 패턴을 어떻게 구현하는지 분석하고, 이를 어떻게 적용할 수 있는지에 대해 알아보겠습니다.
먼저, Pizza 클래스가 어떤 구조로 설계되어 있는지 살펴봅시다.
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE };
final Set<Topping> toppings;
abstract static class Builder {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public Builder addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
public Builder sauceInside() {
return self();
}
abstract Pizza build();
protected abstract Builder self();
}
Pizza(Builder builder) {
toppings = builder.toppings.clone();
}
public String toString() {
return toppings.toString();
}
}
이 클래스는 Pizza 객체를 빌드하는 기본 템플릿을 제공합니다. 중요한 부분은 Builder 클래스입니다. 빌더는 피자에 추가될 토핑을 관리하고, addTopping 메서드를 통해 토핑을 추가할 수 있습니다. 이 클래스는 Pizza 클래스의 추상 클래스로, 각 피자 종류별로 Builder 클래스를 상속받아 구체화해야 합니다.
2. NyPizza: 뉴욕 스타일 피자
다음은 NyPizza 클래스입니다. 이 클래스는 Pizza의 한 종류로, 사이즈가 추가된 예시입니다.
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE };
private final Size size;
public static class Builder extends Pizza.Builder {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
public NyPizza build() {
return new NyPizza(this);
}
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
NyPizza는 Pizza 클래스를 상속받아, Size라는 새로운 속성을 추가합니다. 중요한 부분은 Builder 클래스에서 Size를 필수로 받도록 설계한 점입니다. Builder에서 사이즈를 설정하지 않으면 NullPointerException이 발생하지 않도록 안전하게 Objects.requireNonNull(size)를 사용합니다.
3. Calzone: 칼존 스타일 피자
Calzone 클래스는 또 다른 피자 종류로, 이 피자는 소스가 피자 안에 들어가게 설계되었습니다. 이 클래스를 살펴보겠습니다.
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder {
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
public Calzone build() {
return new Calzone(this);
}
protected Builder self() {
return this;
}
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
public String toString() {
return toppings.toString() + " sauceInside: " + sauceInside;
}
}
Calzone 클래스에서는 sauceInside라는 속성이 추가되어, 피자의 내부에 소스가 들어갈지 여부를 설정할 수 있습니다. Builder 클래스에서 sauceInside 메서드를 호출하여 이 속성을 설정할 수 있습니다. 이처럼 각 피자 종류는 Pizza.Builder 클래스를 상속하여 고유한 속성을 설정할 수 있습니다.
4. PizzaTest: 테스트 코드
마지막으로 PizzaTest 클래스는 실제로 Pizza, NyPizza, Calzone을 빌드하고 출력하는 코드입니다.
public class PizzaTest {
public static void main(String[] args) {
Pizza nyPizza = new NyPizza.Builder(Size.SMALL).addTopping(Topping.SAUSAGE)
.addTopping(Topping.ONION).build();
Pizza calzone = new Calzone.Builder().addTopping(Topping.HAM).addTopping(Topping.PEPPER)
.sauceInside().build();
System.out.println(nyPizza);
System.out.println(calzone);
}
}
이 코드에서는 NyPizza와 Calzone 객체를 각각 빌드한 후, 출력합니다. NyPizza.Builder와 Calzone.Builder를 통해 필요한 속성을 설정하고, build() 메서드를 호출하여 객체를 생성합니다. toString() 메서드를 통해 피자의 토핑과 특성을 출력할 수 있습니다.
5. 결론
이번 분석을 통해 Effective Java에서 권장하는 빌더 패턴을 실제로 어떻게 구현할 수 있는지 살펴보았습니다. 이 패턴은 다양한 인자가 있을 때 객체 생성의 복잡도를 줄여주며, 코드를 읽는 사람이 객체 생성 과정에서 무엇을 설정할 수 있는지 명확히 알 수 있도록 도와줍니다. 특히 피자와 같이 여러 가지 속성을 가진 객체를 만들 때 빌더 패턴을 활용하면 매우 유용합니다.
Java에서 빌더 패턴을 사용할 때는 Abstract Builder를 정의하고, 이를 상속하여 구체적인 객체를 빌드하는 방식이 가장 일반적입니다. 이를 통해 객체 생성의 유연성과 가독성을 높일 수 있습니다.
피자 객체를 만들 때, 다양한 속성(토핑, 소스 포함 여부, 사이즈 등)을 설정하고, 최종적으로 객체를 생성하는 과정을 잘 보여주는 예시로 이 코드를 이해할 수 있었습니다.
'Design Pattern with Java' 카테고리의 다른 글
6. 전략 패턴(Strategy Pattern) (0) | 2025.01.15 |
---|---|
5. 템플릿메소드 패턴(Template Method) (0) | 2025.01.15 |
4. builder 패턴(gof) (0) | 2025.01.07 |
3.추상 팩토리 패턴(Abstract Factory Pattern) (0) | 2025.01.07 |
2.프로토타입 패턴(Prototype Pattern) (0) | 2025.01.07 |