본문 바로가기
Effective Java

item 2

by irerin07 2023. 9. 21.
728x90

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라.

대안 1. 점층적 생성자 패턴 또는 생성자 체이닝

public class NutritionFacts {
  private int servingSize; //필수
  private int servings; //필수
  private int calories; //옵션
  private int fat; //옵션
  private int sodium; //옵션
  private int carbohydrate; //옵션

  public NutritionFacts(int servingSize, int servings) {
    this.servingSize = servingSize;
    this.servings = servings;
    this.calories = 0;
    this.fat = 0;
    this.sodium = 0;
    this.carbohydrate = 0;

  }

  public NutritionFacts(int servingSize, int servings, int calories) {
    this.servingSize = servingSize;
    this.servings = servings;
    this.calories = calories;
    this.fat = 0;
    this.sodium = 0;
    this.carbohydrate = 0;
  }

  public NutritionFacts(int servingSize, int servings, int calories, int fat) {
    this.servingSize = servingSize;
    this.servings = servings;
    this.calories = calories;
    this.fat = fat;
    this.sodium = 0;
    this.carbohydrate = 0;
  }

  public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
    this.servingSize = servingSize;
    this.servings = servings;
    this.calories = calories;
    this.fat = fat;
    this.sodium = sodium;
    this.carbohydrate = 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;
  }
  
}

위 생성자들을 보면 필수값인 servingSize, servings 그리고 옵션값들인 calories, fat, sodium 그리고 carbohydrate가 계속 중복되어 나타난다.

필수값들을 강제할 수 있어 좋지만 코드 중복이 심해 별로 좋아보이지 않는다.

이를 점층적 생성자 패턴 또는 생성자 체이닝을 사용해 조금 더 보기 좋게 수정할 수 있다.

//매개변수가 적은쪽에서 매개변수가 많은쪽의 생성자를 호출해준다.
public class NutritionFacts {
  private int servingSize;
  private int servings;
  private int calories;
  private int fat;
  private int sodium;
  private int carbohydrate;

  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;
  }

}

다만 이런 생성자의 문제점은 해당 클래스의 인스턴스를 만들 때 너무 복잡하고 읽기가 어려워진다.

그나마 IDE의 도움을 받으면 조금은 해결할 수 있겠으나 근본적으로 해결할 수 있는 방법은 아니다.

대안 2. 자바빈즈 패턴

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() {
  }

  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 cocaCola = new NutritionFacts();

cocaCola.setServingSize(24);

하지만 이 방법은 객체의 필수값을 지정하지 않고 사용해버릴 수 있다는 문제가 있다. 즉 일관성이 무너진다는 뜻이다.

이를 해결하기 위해 필수 필드는 생성자로 강제하고 나머지 옵션 필드를 setter를 사용해 지정하도록 할 수 있겠지만 이 방법의 문제점은 객체를 불변객체로 만들기 어렵다는 것이다.

객체 프리즈라는 기능을 구현하여 사용할 수 있겠지만 이는 널리 사용되는 기술은 아니기에 선호하는 방식은 아니다.

권장하는 방법 : 빌더 패턴

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) {
      calories = val;
      return this;
    }

    public Builder sodium(int val) {
      calories = val;
      return this;
    }

    public Builder carbohydrate(int val) {
      calories = 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(240, 8).calories(100).sodium(35).carbohydrate(27).build();

빌더 패턴을 활용하면 인스턴스 생성시 필수값을 강제하면서 옵션 값들은 선택적으로 받을 수 있게 된다.

다만 단점으로는 코드의 양이 많아진다는 것인데 이는 Lombok의 @Builder를 활용하면 쉽게 해결 할 수 있게 된다.

하지만 @Builder 어노테이션도 단점이 있는데, @Builder 어노테이션을 사용하면 자동적으로 모든 패러미터를 받는 생성자를 생성하기 때문에 굳이 Builder를 사용하지 않고서고 인스턴스 생성이 가능해진다.

만약 이를 막고 싶다면 Lobok의 또 다른 어노테이션 @AllArgsConstructor를 사용하되 access = AccessLevel.PRIVATE 옵션을 주어 외부에서 해당 생성자를 사용할 수 없게 만들 수 있다.

또 다른 단점은 인스턴스 생성시 필수값을 지정해줄 수 없다는 것이다. 기존 빌더패턴은 생성자에 필수로 있어야 하는 값을 지정하여 인스턴스 생성시 이를 강제할 수 있었지만 Lombok의 @Builder에는 이를 강제할 수 있는 방법이 아직 없다.

권장하는 방법 : 계층형 빌더

빌더를 계층구조에서 사용하는 방법에 대해 설명한다.

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 builder();

    //하위 클래스는 이 메서드를 재정의(overriding)하여 "this"를 반환하도록 해야 한다.
    protected abstract T self();
  }

  Pizza(Builder<?> builder) {
    toppings = builder.toppings.clone();
  }

}
public class NYPizza extends Pizza{
  public enum Size {SMALL, MEDIUM, LARGE}
  private final Size size;

  public static class Builder extends Pizza.Builder<NYPizza.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);
    this.size = builder.size;
  }

  @Override
  public String toString() {
    return toppings + "로 토핑한 뉴욕피자 사이즈 : " + size;
  }
}
public class Calzone extends Pizza{
  private final boolean sauceInside;
  
  public static class Builder extends Pizza.Builder<Calzone.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 ? "안" : "바깥");
  }
  
}

자바의 final은 레퍼런스 변경이 불가능하게 만든다.

빌더패턴

동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법

복잡한 객체를 만드는 프로세스를 독립적으로 분리할 수 있다.

IllegalArgumentException

잘못된 인자를 넘겨 받았을 때 사용할 수 있는 기본 런타임 예외

checked exception vs unchecked exception

checked exception

  1. 다시 checked exception을 던지거나 try - catch 블록을 사용해 해당 예외를 처리해야 한다.
  2. 컴파일 타임에 체크를 하기 때문에 처리를 하지 않으면 컴파일 할 수 없다.
  3. 복구가 가능한 상황에서 사용한다.

uncheck exception(runtime exception)

  1. 굳이 try - catch 블록을 사용해 잡거나 다시 던지지 않아도 된다.
  2. 클라이언트 코드가 예외가 발생 했을때 뭔가 할 수 없는 상황에서 사용한다.

<aside> 💡 간혹 많은 블로그에서 두 예외의 차이를 Transaction에서 롤백을 한다 하지 않는다로 정리하고 있는데 이는 조금 생각해보면 잘못된 정보이다. https://www.youtube.com/watch?v=_WkMhytqoCc

영상 내용 요약 Transaction은 기본적으로 예외처리를 어떻게 한다는 규칙이 없다.

Transaction은 굉장히 넓은 범위를 커버하고 있는 단어이기 때문에 정확히 어떤 Transaction을 뜻하는 것인지 알아야 한다.

예를 들어 DB Transaction의 경우 checked/unchecked exception 발생시 롤백 여부를 정하는 것은 개발자이다.

많은 블로그에서 인용하는 표의 기원은 스프링의 트랜잭션 처리에서 나온 것이다. 스프링은 기본적으로 Runtime 계열을 바로 롤백을 하고 check exception은 롤백을 하지 않는다. 하지만 이 역시도 개발자가 설정할 수 있다.

</aside>

checked exception의 사용 이유?

예외 발생시에 해당 코드의 클라이언트에게 어떤 액션을 강제하고 싶기 때문

https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html

728x90

'Effective Java' 카테고리의 다른 글

item 4  (0) 2023.10.04
item 3  (0) 2023.09.21
item 1  (0) 2023.09.09