정적 팩터리와 생성자에는 똑같은 제약이 하나 있습니다. 선택적 매개변수가 많을 때 적절히 대응하기가 어렵다는 점입니다.
빌더패턴을 설명하기 이전에 점층적 생성자 패턴(telescoping constructor pattern)에 대해 알아 보겠습니다.
점층적 생성자 패턴은 객체 생성 시 다양한 매개변수 조합을 처리하기 위해 여러 생성자를 제공하는 방식입니다. 이 패턴은 매개변수가 적을 때는 잘 작동하지만, 매개변수의 수가 많아지면 코드의 가독성과 관리가 어려워질 수 있습니다.
이펙티브 자바에서 예시로 이는 점층적 생성자 패턴 예시를 알아볼께요
점층적 생성자 패턴 - 확장하기 어렵다. ( Telescoping Constructor Pattern )
public class NutritionFacts {
private final int servingSize; // (ml, 1회 제공량) 필수
private final int servings; // (회, 총 n회 제공량) 필수
private final int calories; // (1회 제공량당) 선택
private final int fat; // (g/1회 제공량) 선택
private final int sodium; // (mg/1회 제공량) 선택
private final int carbohydrate; // (g/1회 제공량) 선택
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, calories, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int calories1, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = calories1;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
@Override
public String toString() {
return "NutritionFacts{" +
"servingSize=" + servingSize +
", servings=" + servings +
", calories=" + calories +
", fat=" + fat +
", sodium=" + sodium +
", carbohydrate=" + carbohydrate +
'}';
}
public static void main(String[] args) {
NutritionFacts nutritionFacts = new NutritionFacts(240, 8, 100, 0 35, 27);
System.out.println("nutritionFacts = " + nutritionFacts);
}
}
위 코드에서 필수를 제외한 모든 항목은 대부분 0 이다. 보통 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 어쩔 수 없이 그런 매개변수에도 값을 지정해줘야 합니다. 앞 코드에서는 지방(fat)에 0을 넘겼습니다. 이 예에서는 매개변수가 6개 뿐이라 그리 나빠 보이지 않을 수 있지만 ( 전 6개도 어질어질 합니다 ) 수가 더 늘어나면 금세 걷잡을 수 없이 헷갈리게 됩니다.
요약하자만 점층적 생성자 패턴도 쓸 수 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵습니다.
점층적 생성자 패턴의 또다른 예시
public class Coffee {
private final String type;
private final int sugar;
private final int milk;
private final int size;
// 기본 생성자
public Coffee(String type) {
this(type, 0);
}
// 설탕을 추가하는 생성자
public Coffee(String type, int sugar) {
this(type, sugar, 0);
}
// 설탕과 우유를 추가하는 생성자
public Coffee(String type, int sugar, int milk) {
this(type, sugar, milk, 250);
}
// 모든 매개변수를 받는 생성자
public Coffee(String type, int sugar, int milk, int size) {
this.type = type;
this.sugar = sugar;
this.milk = milk;
this.size = size;
}
@Override
public String toString() {
return "Coffee{" +
"type='" + type + '\'' +
", sugar=" + sugar +
", milk=" + milk +
", size=" + size +
'}';
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
Coffee blackCoffee = new Coffee("Black");
Coffee sweetCoffee = new Coffee("Latte", 2);
Coffee sweetMilkyCoffee = new Coffee("Cappuccino", 1, 1);
Coffee customCoffee = new Coffee("Espresso", 0, 0, 100);
System.out.println(blackCoffee);
System.out.println(sweetCoffee);
System.out.println(sweetMilkyCoffee);
System.out.println(customCoffee);
}
}
해당 예시에서도 Coffee 클래스는 여러 public 생성자를 가지고 있으며, 각 생성자는 다른 조합의 매개변수를 받습니다. 이 방식은 각각의 생성자가 다른 매개변수 조합을 처리할 수 있도록 하지만, 매개 변수 수가 늘어날수록 복잡성이 대단히 증가합니다.
자반빈즈 패턴 ( JavaBeans Pattern )
이번에는 선택 매개변수가 많을 때 활용할 수 있는 두번째 대안인 자바빈즈 패턴(JavaBeans Pattern)을 알아 보겠습니다. 매개변수가 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식입니다.
public class NutritionFactsJavaBeansPattern {
private int servingSize = -1; // (ml, 1회 제공량) 필수
private int servings = -1; // (회, 총 n회 제공량) 필수
private int calories = 0; // (1회 제공량당) 선택
private int fat = 0; // (g/1회 제공량) 선택
private int sodium = 0; // (mg/1회 제공량) 선택
private int carbohydrate = 0; // (g/1회 제공량) 선택
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
@Override
public String toString() {
return "NutritionFactsJavaBeansPattern{" +
"servingSize=" + servingSize +
", servings=" + servings +
", calories=" + calories +
", fat=" + fat +
", sodium=" + sodium +
", carbohydrate=" + carbohydrate +
'}';
}
public static void main(String[] args) {
NutritionFactsJavaBeansPattern nutritionFacts = new NutritionFactsJavaBeansPattern();
nutritionFacts.setServingSize(240);
nutritionFacts.setServings(8);
nutritionFacts.setCalories(100);
nutritionFacts.setFat(10);
nutritionFacts.setSodium(35);
nutritionFacts.setCarbohydrate(27);
System.out.println("nutritionFacts = " + nutritionFacts);
}
}
점층적 생성자 패턴의 단점들이 자바빈즈 패턴에서는 더 이상 보이자 않습니다. 자바 빈즈 패턴에서는 setter 메서드를 호출하여 각각의 필드에 값을 넣는 방식을 사용하기 때문입니다. 이렇게 하면 어떤 값을 넣을때 필요한 항목만 넣고 사용할 수 있게 되므로 복잡성일 줄어듭니다. 하지만, 심각한 단점으로 자바빈즈 패턴에서는 객체 하나를 만들여면 메서드를 여러개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency) 이 무너진 상태에 놓이게 됩니다.
바로 이런 문제점을 해결하기 위해 이펙티브 자바에서는 "생성자 매개 변수가 많다면 빌더를 고려하라" 라고 합니다.
우리가 흔히 Lombok에서 사용하는 @Builder 는 이런 문제점을 해결해주는 빌더 패턴입니다.
빌더 패턴은 점층적 생성자 패턴과 자바빈스 패턴의 장점만 취했습니다.
위에서 예시로 만든 Coffee 클래스를 빌더 패턴으로 만들겠습니다.
빌더패턴 ( Builder Pattern , Method Chaining )
// 외부 클래스
public class Coffee {
private final String type;
private final int sugar;
private final int milk;
private final int size;
private Coffee(Builder builder) {
this.type = builder.type;
this.sugar = builder.sugar;
this.milk = builder.milk;
this.size = builder.size;
}
// 정적 내부 클래스
public static class Builder {
private String type;
private int sugar;
private int milk;
private int size;
public Builder type(String type) {
this.type = type;
return this;
}
public Builder sugar(int sugar) {
this.sugar = sugar;
return this;
}
public Builder milk(int milk) {
this.milk = milk;
return this;
}
public Builder size(int size) {
this.size = size;
return this;
}
public Coffee build() {
return new Coffee(this);
}
}
@Override
public String toString() {
return "Coffee{" +
"type='" + type + '\'' +
", sugar=" + sugar +
", milk=" + milk +
", size=" + size +
'}';
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
Coffee coffee = new Coffee.Builder()
.type("Latte")
.sugar(1)
.milk(2)
.size(300)
.build();
System.out.println(coffee);
}
}
이 예시에서 Coffee 클래스는 여러 매개변수를 가지고 있지만 Builder 내부 클래스를 통해 객체를 단계적으로 구성할 수 있습니다.
클라이언트는 Coffee 객체를 생성할 때 필요한 매개변수만 설정하면 됩니다.
이를 메서드 체이닝이라고도 합니다. 메서드 체이닝은 객체의 메서드들을 연속적으로 호출하는 코딩 스타일로, 각 메서드가 객체 자신(this)을 반환함으로써 가능해집니다. 이 방식은 코드를 더 읽 쉽고 간결하게 만들며, 특히 위와같이 빌더패턴에서 자주 사용됩니다.
정적 내부 클래스란?
정적내부 클래스는 외부 클래스 내부에 정의 되지만, 외부 클래스의 인스턴스와는 독립적입니다. 쉽게 말해 메모리 로드가 다릅니다.
이 부분은 JAVA 에서 다시 다루도록 하겠습니다.
'이펙티브 자바' 카테고리의 다른 글
불필요한 객체 생성을 피하라 (0) | 2024.01.11 |
---|---|
자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2024.01.10 |
인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.01.03 |
private 생성자나 열거타입으로 싱글턴임을 보증하라 (0) | 2024.01.03 |
정적 팩터리 메서드 (1) | 2023.12.20 |