이펙티브 자바의 아이템2. '생성자에 매개변수가 많다면 빌더를 고려하라' 부분을 읽고 정리하는 포스팅.✍️
만약 필드의 갯수가 많고, 그 필드를 모두 생성자를 통해 받아야 하는 상황이라면?🤔
점층적 생성자 패턴
필수 매개변수만 받는 생성자, 필수 매개변수 + 선택 매개변수 1개 받는 생성자, 필수 매개변수 + 선택 매개변수 2개 받는 생성자 ...
이렇게 생성자를 늘려가는 패턴.
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 static void main(String[] args) {
NutritionFacts cocaCola =
new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}
원하는 생성자를 호출해서 인스턴스화한다.
보통 이런 생성자 구조는 사용자가 원하지 않는 매개변수까지 포함해서 넣어줄 확률이 높다.
그리고 매개변수의 수가 늘어나면 그만큼 많은 생성자를 만들어내야 함으로 생산성도 좋지 않다.
같은 타입의 매개변수가 연달아 들어가면 찾기 어려운 버그가 발생할 수도 있다.
(컴파일 에러가 발생하지 않기 때문에 런타임에서 에러가 날 수 있음)
자바빈즈 패턴
매개변수가 없는 생성자로 객체를 만든 후, setter메서드를 호출해서 원하는 매개변수에 값을 설정하는 방식.
public class NutritionFacts {
// 매개변수들은 (기본값이 있다면) 기본값으로 초기화된다.
private int servingSize = -1; // 필수; 기본값 없음
private int servings = -1; // 필수; 기본값 없음
private int calories = 0;
private int fat = 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 static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
...
}
}
자바빈즈 패턴에서는 객체 하나를 만드려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.(불변식을 검증할 수 없음)
버그를 심는 코드와 그 버그 때문에 런타임 에러를 겪는 코드가 서로 떨어져 있을 것이므로 디버깅도 힘들다.
그리고 자바빈즈 패턴에서는 클래스를 불변으로 만들 수도 없다.
위 단점을 완화하고자 freezing이라는 방법을 사용하는데, 생성이 끝난 객체를 수동으로 얼어있는 상태로 만들고, 얼어있기 전에는 그 객체를 사용할 수 없게 만드는 것이다. 하지만, freeze메서드를 확실히 호출했는 지를 컴파일러가 보증해주지 않기 때문에 런타임 오류엔 마찬가지로 취약하다.
그래서 등장한 빌더 패턴!
점층적 생성자 패턴의 안정성 + 자바빈즈 패턴의 가독성을 겸비
빌더패턴은 다음 순서대로 구현한다.
- 클라이언트가 필요한 객체를 만들지 않고, 필수 매개변수만으로 빌더 객체를 얻는다.
- 그리고 빌더 객체가 제공하는 일종의 setter 메서드로 원하는 선택 매개변수를 설정한다.
- 마지막으로 build메서드를 호출해서 필요한 객체를 얻는다.
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만드는 것이 일반적이다.
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 NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
...
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
}
}
빌더 패턴으로 구현할 경우 모든 매개변수의 기본값을 한 곳에 모아둘 수 있다.
빌더 패턴은 메서드 연쇄 방식으로 사용되며, 코드를 쓰기 쉽고, 읽기 쉽다.
또한, 빌더를 사용하면 가변인수 매개변수를 여러 개 사용할 수도 있다. 각각을 적절한 메서드로 나눠서 선언한다.
(위 코드의 addTopping메서드가 그 예시)
빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build가 호출하는 생성자에서 불변식을 검사한다.
계층적으로 설계된 클래스에서 빌더 패턴 사용하기
각 계층의 클래스에 관련 빌더를 멤버로 정의한다.
추상 클래스는 추상 빌더, 구체 클래스는 구체 빌더
Pizza
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();
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
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);
}
@Override protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
@Override public String toString() {
return toppings + "로 토핑한 뉴욕 피자";
}
}
Calzone
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 ? "안" : "바깥");
}
}
사용 코드
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
각 하위 클래스는 해당하는 구체 하위 클래스를 반환하도록 공변 반환 타이핑을 한다.
공변 반환 타이핑: 상위 클래스의 메서드를 오버라이드 할 때 리턴 타입을 그 리턴 타입의 하위 타입으로 정의하는 것.
빌더를 만들 때 고려할 점
- 객체를 만들 때 빌더부터 만들어야 함. 성능에 민감하다면 문제가 될 수 있다.
- 점층적 생성자 패턴보다 코드가 장황해서 생성자 매개변수가 4개 이상부터 고려할만 하다. 생산성 문제.
- 하지만, 추후 API를 변경할 때 매개변수가 추가될 수도 있고, 생성자나 정적 팩토리 방식으로 시작했다가 나중에 빌더 패턴을 적용시킬 경우 이미 만들어둔 생성자와 정적 팩토리가 통일성을 해치게 되니 처음부터 빌더로 시작하는 것이 나을 수 있다.
생성자나 정적 팩터리 메서드가 처리해야할 매개변수가 많다면 빌더 패턴을 선택하자.
끗.
'Java' 카테고리의 다른 글
Collections.unmodifiableXX()는 방어적 복사가 아니다 (1) | 2022.03.06 |
---|---|
Collection의 깊은 복사와 방어적 복사 (2) | 2022.03.05 |
추상 클래스와 인터페이스의 용도 차이 (0) | 2022.03.03 |
IntelliJ(인텔리제이) 커스텀 자동완성 템플릿 만들기 - Live Template (2) | 2022.02.18 |
Java String.split() 빈 문자열도 포함하는 방법 (0) | 2022.02.15 |