[Effective Java] 아이템 2 : 생성자에 매개변수가 많다면 빌더패턴을 고려하라

HyeBin, Park·2022년 4월 2일
0

Effective Java Study

목록 보기
2/20
post-thumbnail

매개변수가 많을때 어떻게 하면 좋을까 ?

1. 생성자 사용

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 sodium, int fat, int carbohydrate, int servings) {
  		this.servingSize = servingSize;
        this.sodium = sodium;
        this.fat = fat;
        this.carbohydrate = carbohydrate;
        this.servings = servings;
  }
}
public class Main() {
	public static void main(String[] args) {
    	NutritionFacts nutritionFacts = new NutritionFacts(140, 8, 100, 0, 35);
	}
}
  • 생성자를 사용할 수 있지만 Main 코드만 봤을때 생성자 안의 인자값이 무엇을 의미하는지 알 수 없다.

2. setter 사용하기

chip.setServingSize(100);
chip.setServings(1);
  • 매개변수를 받지 않는 생성자를 사용해서 인스턴스를 만들고, setter를 사용해서 필요한 필드만 설정하기
  • 인자값이 무엇을 의미하는지 알 수 있다.
  • 여러번의 호출을 거쳐야 해서 자바빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다.
  • 불변 클래스로 만들지 못한다는 단점이 있다. (쓰레드 간에 공유 가능한 상태가 있음) => 스레드 안정성 x
  • 스레드 안전성 보장을 위해서는 추가적인 수고가 필요하다.
  • Freezing ? : 객체에 새로운 속성을 추가할 수 없고, 원래 존재하던 속성을 제거할 수 없으며 즉 불변화 시킬 수 있다. ? => 자바스크립트에는 있는데 자바에는 잘 모르겠다 .. JavaScript

자바빈즈

  • 자바에서 작성된 컴포넌트들을 일컫는 말
  • 자바 클래스로서 값을 가지는 속성과 setter, getter 로 이루어져 있음

3. Builder 패턴


public class NutritionFacts {

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

  private NutritionFacts(Builder builder) {
      servingSize = builder.servingSize;
      servings = builder.servingSize;
      calories = builder.calories;
      fat = builder.fat;
      sodium = builder.sodium;
      carbohydrate = builder.carbohydrate;
    	
      }
}

👉 fluent API 혹은 메서드 연쇄

  • 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.
  • 쓰기 쉽고, 읽기 쉽다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
		.calories(100).sodium(35).carbohydrate(27).build();
  • 빌더 패턴은 명명된 선택적 매개변수를 흉내낸 것 이다. (C#의 예제)

  • 유효성 검사가 가능하다.


 public Builder sodium(int val) {
          if (val == null || val.equals("")) 
                     throw new IllegalArgumentException("sodium is empty");
 
          sodium = val;
          return this;
  }
  • 공격에 대비해 이런 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야한다.

👉 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

  • 각 계층의 클래스에 관련 빌더를 멤버로 정의한다.

  • 추상 클래스는 추상 빌더를 갖게한다.

public abstract class Pizza {
	public enum Topping {HAM, MUSHROOM, ONION, PEPPER, OLIVE}
    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();
        
        // 시뮬레이트한 셀프 타입 관용구 => 자바에는 self 타입이 없어 우회 방법 
        protected abstract T self(); // 하위 클래스에서는 형변환 없이 메서드 연쇄 지원 가능 
    }
    // 생성자에서 빌더를 받음
    Pizza(Builder<?> builder) {
    	toppings = builder.toppings.clone();
    }
}

🤷‍♀️ Objects.requireNonNull();

public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }
  • 구체 클래스는 구체 빌더를 갖게 한다.
public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;

    private NyPizza(Builder builder) {
        super(builder); // 토핑을 세팅
        size = builder.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;
        }
    }

}
public class Calzone extends Pizza {
    private final boolean sauceInside;

    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false;

        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }
		
         // 공변 반환 타이핑 : 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌
        // 하위 타입을 반환하는 기능 
        @Override
        Pizza build() {
            return new Calzone(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

}
  • 가변인수 배개변수를 여러개 사용할 수 있다.
  • 공변 반환 타이핑을 사용하면 클라이어느가 형 변환에 신경 쓰지 않고도 빌더를 사용할 수 있다.
NyPizza pizza = new NyPizza.Builder(SMALL)
			.addTopping(HAM).addTopping(ONION).build();
            
Calzone calzone = new Cazone.Builder()
			.addTopping(HAM).sauceInside().build();

단점

  • 객체를 만들기 전에 먼저 빌더를 만들어야해서 성능에 민감한 상황에서 문제
  • 생성자를 사용하는 것 보다 코드가 장황하다.

0개의 댓글