티스토리 뷰
static 팩토리 메소드와 생성자에는 똑같은 제약이 있다.
이는 입력 받는 파라미터의 개수가 많다면 코드를 알아보기 힘든 매우 화가 나는 상황이 생긴다.
전통 적으로 점층적 생성자(telescoping constructor) 패턴을 사용했다.
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, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola =
new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}
원하는 파라미터를 포함하는 생성자 중 가장 짧은걸 골라서 쓰면 된다.
이때 사이사이 필요 없는 값들에 0 같은 값을 넣어 주어야 하고
파라미터가 많아질수록 개발자의 실수로 예기치 못한 버그를 생성할 우려가 있다.
이를 해결할 대안으로 나온 자바빈즈(javaBeans) 패턴이 있다.
그냥 늘 쓰던 setter 만들기이다.
public class NutritionFacts {
// 매개변수들은 (기본값이 있다면) 기본값으로 초기화된다.
private int servingSize = -1; // 필수; 기본값 없음
private int servings = -1; // 필수; 기본값 없음
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// Setters
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
이때 객체를 완성하기 위해 호출되는 메서드가 너무 많다.
클래스가 완성되기 이전 사용될 수 있기 때문에 일관성이 무너지게 된다.
setter를 만들었기 때문에 immutable 한 class 구조를 설계할 수 없다.
thread safe 하지 않다. 정도가 되겠다.
이 두 패턴의 장점을 결합한 빌더(Builder) 패턴이 존재한다. 야호!
Lombok이 만들어줘서 그냥 썼었는데 다양한 장점이 존재한다.
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수 - 기본값으로 초기화한다.
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
}
}
static inner 클래스로 Builder 클래스를 선언하면 Outer 클래스의 인스턴스를 생성하지 않고
Builder의 메소드를 호출 가능하다.
Builder의 매개변수를 추가하는 메소드들의 return을 Builder의 레퍼런스로 하여
메소드들을 chaining 해 사용할 수 있게 된다.
이렇게 구성하면 setter를 생성하지 않았기에 immutable 한 클래스를 생성할 수 있다.
이때 Builder의 method들에서 매개변수의 유효성 검사를 진행하고 올바르지 않은 매개변수가 들어오면
Exception을 던지는 코드를 쓸 수 있다.
점층적 생성자 패턴에서 파라미터마다 if문을 걸어 검사하는 것보다 훨씬 가독성이 좋을 것 같다.
실제로 서비스가 크면 DTO 같은 친구들을 만들 때도 Exception을 던지는 것을 고려해 봐야겠다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 재정의(overriding)하여
// "this"를 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // 아이템 50 참조
}
}
Collection api를 뜯어볼 때 봤던 extends generic이다. 무슨 문법인지 잘 몰라 미래에서 아이템 30을 읽고 왔다.
// 재귀적 타입 한정을 이용해 상호 비교할 수 있음을 표현 (179쪽)
public class RecursiveTypeBound {
// 코드 30-7 컬렉션에서 최댓값을 반환한다. - 재귀적 타입 한정 사용 (179쪽)
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return result;
}
public static void main(String[] args) {
List<String> argList = Arrays.asList(args);
System.out.println(max(argList));
}
}
Java로 알고리즘 풀 때 구현 해본 Comparable이다.
" <E extends Comparable<E>> "의 의미는 Comparable<E>를 구현한 Type E만을 받겠다는 이야기이다.
Interface Comparable은 method compareTo가 존재한다.
Type E가 Comparable을 implements 했으면 compareTo method가 존재하게 된다.
따라서 아래 for each 구문 e에서 method compareTo를 호출할 수 있게 된다.
그러면 다시 원래 코드로 돌아와서 이해를 해보자.
abstract class인 Pizza를 상속받으면 abstract sub class인 Builder를 반드시 구현해야 한다.
그런데 클라이언트에서 Builder를 지 맘대로 구현할 수 있다.
"Builder<T extends Builder<T>>"의 의미는
Builder를 구현할 때 Pizza.Builder를 상속받아서 쓰거라~ 라고 해석할 수 있다.
실제 구현 코드를 살펴보면,
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // 기본값
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
@Override public String toString() {
return String.format("%s로 토핑한 칼초네 피자 (소스는 %s에)",
toppings, sauceInside ? "안" : "바깥");
}
}
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override public NyPizza build() {
return new NyPizza(this);
}
@Override protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
@Override public String toString() {
return toppings + "로 토핑한 뉴욕 피자";
}
}
Pizza를 상속받은 칼초네 핏짜와 뉴욕 핏짜이다.
Pizza.Builder의 builde와 self method를 재정의해 사용하는 것을 볼 수 있다.
self method는 상위 타입에서 지정한 return type에 한정되는 것이 아니라 제네릭을 사용했기에
형변환을 생각하지 않고 쓸 수 있다. 정말 유연한 것 같다.
public class PizzaTest {
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
System.out.println(pizza);
System.out.println(calzone);
}
}
빌더 패턴이 다양한 장점이 존재하지만 단점도 존재한다.
class의 instance를 생성하기 이전에 static class를 생성해야 하므로 성능이 조금 떨어질 수 있다.
그리고 코드가 장황하다 정도?.. Lombok을 써야겠다 하하!
느낀 점 : class를 설계하는 과정은 무언가 framework API 개발 같은 곳에 많이 사용될 것 같다.
코드 출처 :
GitHub - WegraLee/effective-java-3e-source-code: 『이펙티브 자바, 3판』(인사이트, 2018)
『이펙티브 자바, 3판』(인사이트, 2018). Contribute to WegraLee/effective-java-3e-source-code development by creating an account on GitHub.
github.com
GitHub - WegraLee/effective-java-3e-source-code: 『이펙티브 자바, 3판』(인사이트, 2018)
『이펙티브 자바, 3판』(인사이트, 2018). Contribute to WegraLee/effective-java-3e-source-code development by creating an account on GitHub.
github.com
'Book > Effective Java' 카테고리의 다른 글
Effective Java - Item 6. 불필요한 객체 생성을 피하라 (0) | 2023.01.05 |
---|---|
Effective Java - Item 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.01.03 |
Effective Java - Item 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2023.01.02 |
Effective Java - Item 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2023.01.02 |
Effective Java - Item 1. 생성자 대신 static 팩토리 메소드를 고려해 볼 것 (0) | 2022.12.24 |