본문은 Effective Java를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
정적 팩터리 메서드와 생성자에는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 공통적인 제약이 있다. 이에 따른 해결방안으로 여러가지 해결책을 소개하고자 한다.
해결책 1: 점층적 생성자 패턴
먼저 필수 매개변수만 받는 생성자를 지정하고, 필요에 따라 선택 매개변수를 추가하는 점층적 생성자 패턴이 있다.
// IDE의 도움 없이는 읽기가 상당히 불편하다..
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
- 이런 생성자를 쓰다보면 필요하지 않은 매개변수를 포함하는 경우가 발생하며, 심지어는 거기에 값을 지정해야 한다는 불편함이 발생한다.
- 또한, 매개변수의 개수가 너무 많아지면 코드의 양도 증가할 것이며 매개변수의 순서도 헷갈릴 것이다.
- 다시 말해, 이런 코드는 작성하기도 어렵고 읽기도 어렵다.
해결책 2: 자바빈즈 패턴
두번째로는 매개변수가 없는 생성자로 객체를 만든 후, 세터를 사용해 필드의 값을 설정하는 자바빈즈 패턴이 있다. 이 방법은 해결책1에 비해 가독성이 뛰어나다는 장점이 있다.
// 매개변수가 없는 생성자
NutritionFacts cocaCola = new NutritionFacts();
// 세터를 이용해 필드값 세팅
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100); // 여기서 사용한다면..? 불완전하다!
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
- 이 방법의 단점은 객체 하나를 만들기 위해선 메서드를 여러 번 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓인다는 단점이 있다.
- 쉽게 말해서 자바빈즈 패턴을 따른 객체가 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다.
- 또한, 불변 클래스로 만들지 못한다는 단점이 있고, 쓰레드 안정성을 보장하기 위해선 추가적인 작업이 필요하다는 단점이 있다.
해결책 3: 빌더 패턴
세번째는 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 모두 갖춘 빌더 패턴이다.
빌더 패턴은 필요한 객체를 직접 만드는 대신,
1) 빌더(생성자 또는 정적 팩터리)에 필수적인 매개변수를 주면서 호출하여 `Builder` 객체를 얻은 다음
2) 빌더 객체가 제공하는 세터와 비슷한 메소드를 사용해서 부가적인 필드를 채워넣고
3) 최종적으로 `build`라는 메소드를 호출해서 만들려는 객체를 생성한다.
// 빌더 패턴 적용
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
빌더 패턴으로 C#, 파이썬, 스칼라 등이 제공하는 `Named Optional Parameter`를 모방할 수 있다.
// C#
PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");
PrintOrderDetails(productName: "Red Mug", sellerName: "Gift Shop", orderNum: 31);
- 빌더의 생성자나 메소드에서 유효성 확인을 할 수도 있고 여러 매개변수를 혼합해서 확인해야 하는 경우에는 `build` 메소드에서 호출하는 생성자에서 할 수 있다.
- 빌더에서 매개변수를 객체로 복사해온 다음에 확인하고, 검증에 실패하면 `IllegalArgumentException`을 던지고 에러 메시지로 어떤 매개변수가 잘못됐는지 알려줄 수 있다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에서는 추상 클래스를 상속받으며 각 하위 클래스용 빌더도 추상 빌더를 상속받아 만들 수 있다.
추상 빌더를 지닌 추상 클래스 Pizza
public abstract class Pizza {
public enum Topping {
HAM, MUSHROOM, ONION, PEEPER, SAUSAGE
}
final Set<Topping> toppings;
// 자기 자신의 하위타입을 받는 빌더 -> 재귀적인 타입 매개변수(아이템30), https://st-lab.tistory.com/153 참고
// T에는 Builder<T> 클래스 자신을 포함한 그 하위 클래스 타입들이 모두 올 수 있다
// addTopping이라는 메서드를 제공하는 빌더
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(); // 빌더의 하위타입이 계속해서 넘겨지도록 하기 위함
}
// Pizza의 하위타입을 생성 (abstract class)
// 메서드를 구현할 때 NyPizza와 Calzone의 생성자를 호출함
// new NyPizza(this), new Calzone(this);
abstract Pizza build();
// 자기 자신(빌더)를 반환, 메소드 체이닝이 가능케 함 -> 자바에는 없는 self타입을 구현하기 위함
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
Pizza를 상속하는 NyPizza
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); // 클라이언트측에서 형변환할 필요가 X
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder); // toppings 세팅
size = builder.size; // size 세팅
}
}
Pizza를 상속하는 Calzone
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauseInside = false;
public Builder sauceInde() {
sauseInside = 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.sauseInside;
}
}
NyPizza nyPizza = new NyPizza.Builder(SMALL) // 필수적인 매개변수를 넣고
.addTopping(Pizza.Topping.SAUSAGE) // 부가적인 매개변수를 넣는다
.addTopping(Pizza.Topping.ONION)
.build();
Calzone calzone = new Calzone.Builder()
.addTopping(Pizza.Topping.HAM)
.sauceInde()
.build();
- 빌더는 가변인수 (vargars) 매개변수를 여러개 사용할 수 있다는 사소한 이점이 있다
- 생성자나 팩토리는 가변인자를 마지막 매개변수에 한번밖에 못쓰므로
- 또한 위의 addTopping() 메서드를 여러번 호출하는 예제에서 본것처럼 여러 메소드 호출을 통해 전달받은 매개변수를 모아 하나의 필드에 담는 것도 가능하다.
- 빌더 패턴은 유연해서 빌더 하나로 여러 객체를 생성할 수도 있고 매번 생성하는 객체를 조금씩 변화를 줄 수도 있다.
- 예를 들어, 객체마다 부여되는 일련번호와 같은 필드는 빌더가 채우게 만들 수 있다.
- 단점으로는 객체를 만들기 전에 먼저 빌더를 만들어야 하는데 성능에 민감한 상황에서는 그점이 문제가 될 수도 있다고는 한다(실제로는 미미하지 않을까 싶다).
- 또한, 빌더 패턴은 생성자를 사용하는 것보다 코드가 더 장황하기 때문에(클라이언트 기준X) 매개변수가 많거나(4개 이상) 또는 앞으로 늘어날 가능성이 있는 경우에 사용하는 것이 좋다.
결론
- 생성자나 정적 팩터리가 처리해야 할 매개변수가 많거나 추후 추가될 것 같다면 빌더 패턴을 선택해라
- 매개변수 중 다수가 필수가 아니거나 같은 타입이라면 더더욱 빌더를 써라
참고
https://github.com/keesun/study/blob/master/effective-java/item2.md
https://www.youtube.com/watch?v=OwkXMxCqWHM&t=1778s&ab_channel=%EB%B0%B1%EA%B8%B0%EC%84%A0
https://st-lab.tistory.com/153
'책 > Effective Java' 카테고리의 다른 글
[이펙티브 자바] 아이템6: 불필요한 객체 생성을 피하라 (0) | 2022.04.18 |
---|---|
[이펙티브 자바] 아이템5: 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.04.18 |
[이펙티브 자바] 아이템4: 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2022.04.18 |
[이펙티브 자바] 아이템3: private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2022.04.18 |
[이펙티브 자바] 아이템1: 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.04.18 |