반응형
생성자와 정적 팩터리는 매개변수가 많을 때 적절히 대응하기 어렵다.
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 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;
}
}
아래와 같은 생성자는 사용자가 설정하길 원치 않는 매개변수에도 값을 지정해줘야 한다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
정적 팩터리와 생성자에 선택적 매개변수가 많을 때 사용할 수 있는 방법들
1. 점층적 생성자 패턴 또는 생성자 체이닝
public class NutritionFacts {
...
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;
}
}
단점
매개변수가 늘어날수록 코드를 작성하거나 읽기 어렵다.
- 각 값의 의미가 무엇인지 헷갈린다.
- 타입이 같은 매개변수가 연달아 늘어서 있으면 찾기 어려운 버그로 이어질 수 있다.
- 파라미터가 같은 경우 순서가 바뀌게 컴파일러는 알아채지 못하고 런타임에 버그가 발생할 수 있다.
2. 자바빈즈패턴(javaBeans pattern)
자바 빈즈는 자바 표준 스펙 중의 하나로 클래스의 필드에 대한 getter, setter 네이밍 패턴을 정의한 표준 스펙 중 하나이다.
public class NutritionFacts {
private int servingSize; // 필수
private int servings; // 필수
private int calories; // 선택
private int fat; // 선택
private int sodium; // 선택
private int carbohydrate; // 선택
public NutritionFacts() {}
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;
}
}
장점
코드가 길어지긴 했지만 인스턴스를 만들기 쉽고 코드를 읽기 쉽다.
{
NutritionFacts cocalCola = new NutritionFacts();
cocalCola.setServingSize(240);
cocalCola.setServings(8);
cocalCola.setCalories(100);
cocalCola.setSodium(35);
cocalCola.setCarbohydrate(27);
}
- 매개변수가 없는 생성자로 객체를 만든 후 세터(setter) 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.
단점
객체 하나를 만들려면 메서드를 여러개 호출해야 한다.
- 객체가 완전히 생성되기 전까지 일관성이 깨지고, 불변으로 만들 수 없다.
어느 값까지 세팅되어야하는지 알기가 어려워진다
- 런타임에 문제가 발생하면 디버깅이 어렵다.
- 주석을 남기고 문서화하는 방법밖에 없다.
필수 필드값은 생성자로 넘겨받도록 강제하고 선택할 수 있는 값들은 세터를 통해 넘겨받도록 할 수 있다.
public class NutritionFacts {
private int servingSize; // 필수
private int servings; // 필수
private int calories; // 선택
private int fat; // 선택
private int sodium; // 선택
private int carbohydrate; // 선택
//생성자 1개 필수값 전부 받기
public NutritionFacts(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public void setCalories(int calories) {...}
public void setFat(int fat) {...}
public void setSodium(int sodium) {...}
public void setCarbohydrate(int carbohydrate) {...}
}
- setter가 존재하기 때문에 불변으로 만들 수 없다.
불변으로 만들 수 없어 스레드 안정성을 얻기 위해서 프로그래머는 추가 작업을 해줘야 한다.
- 자바스크립트에 Object.freeze()처럼 생성이 끝난 객체는 수동으로 freezing 해주고 freezing 하기 전에는 사용할 수 없도록 다루는 방법이 있지만 다루기 어려워 실전에서 사용하지 않는다.
3. 빌더 패턴(Builder pattern)
- 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 챙길 수 있다.
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;
}
}
{
NutritionFacts cocaCola = new Builder(1, 10)
.calories(240)
.sodium(35)
.build();
}
- 빌더 메서드는 자신을 반환하기 때문에 연쇄적으로 호출이 가능하다.
- 플루언트 API 혹은 메서드 연쇄(method chaining)라 한다.
장점
모든 매개변수의 기본값들을 한곳에 모아두면서 불변을 챙길 수 있다.
- 생성자의 매개변수도 줄어들고 객체도 안전하게 사용할 수 있게된다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
- 각 계층의 클래스에 관련 빌더를 멤버로 정의하고 추상 클래스는 추상 빌더를 구체 클래스는 구체 빌더를 갖게 한다.
public abstract class Noodle {
public enum Topping {HAM,EGG,BACON,CHEESE}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
private EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Noodle build();
protected abstract T self();
}
Noodle(Builder<?> builder) {
toppings = builder.toppings;
}
}
class Ramen extends Noodle{
public enum Size {
SMALL,
MEDIUM,
LARGE
}
private final Size size;
public static class Builder extends Noodle.Builder<Ramen.Builder> {
private final Size size;
public Builder(Size size) {
this.size = size;
}
@Override
public Ramen build() {
return new Ramen(this);
}
@Override
protected Builder self() {
return this;
}
}
Ramen(Builder builder) {
super(builder);
this.size = builder.size;
}
}
class Main {
public static void main(String[] args) {
Ramen ramen = new Ramen.Builder(MEDIUM)
.addTopping(EGG)
.addTopping(HAM)
.build();
}
}
- Self() 라는 매커니즘을 이용해 빌더를 만들었을 때는 별도의 캐스팅이 필요없게 된다.
빌더 패턴은 빌더 하나로 여러 객체를 순회하면서 만들 수 있기 때문에 상당히 유연하다.
- 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.
단점
객체를 만들려면 빌더부터 만들어야 한다. 성능에 민감한 상황에서는 문제가 될 수 있다.
저자의 의견
- 매개변수가 4개는 넘어야 값어치를 한다.
- API는 시간이 지날수록 매개변수가 많이지는 경향이 있어 처음부터 빌더로 시작하는 편이 나을 때가 많다.
Spring Boot로 Lombok을 사용하면 @Builder를 제공하기 때문에 Builder를 사용하는 것도 괜찮은거 같다.
반응형
'책 > 이펙티브 자바' 카테고리의 다른 글
item6 - 불필요한 객체 생성을 피하라 (0) | 2024.02.25 |
---|---|
item3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2024.02.18 |
Item1 - 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2024.02.15 |