Effective Java Ch2. 객체의 생성과 소멸 - Item 2

Donghyeok Jang·2021년 8월 20일
0

Effective Java

목록 보기
2/4

Item 2. 생성자 매개변수가 많은 경우 빌더 사용을 고려해 볼 것.

생성자, static 팩토리 메소드 는 공통적인 제약사항이 존재한다. 두 방법 모두 optional한 매개변수들을 scale하지 못한다.


해결책 1. 생성자

NutritionFact라는 클래스를 예시로 들고있다. 해당 클래스는 반드시 필요한 필드와 Optional한 필드를 가질 수 있는데, 이 경우 필수적인 매개변수를 가진 생성자에 부가적인 필드를 하나씩 추가해 여러 생성자를 만들 수 있다. 이렇게 생성자를 쌓아 올려가며 생성자를 구성하는 것을 Telescoping constructor pattern이라고 한다.

// 인스턴스 생성
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100 0, ...);
클래스
// Telescoping constructor Pattern
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, servigns, 0);
  	}
  
  	public NutritionFacts(int servingSize, int servings, int calories) {
  		this(servingSize, servigns, calories, 0);
  	}
  	
  	public NutritionFacts(int servingSize, int servings, int calories, int fat) {
  		// recursive constructor의 최종 생성자
  		this.servingSize = servingSize;
  		this.servings = servings;
  		this.calories = calories;
  		this.fat = fat;
  	}
  	// ...
	
}

단점

  • (IDE에서 매개변수의 이름을 제공해주는 경우도 있지만 일반적으로) 매개변수가 많을 수록 코드의 가독성이 떨어지고, 쓰기도 힘들다.

해결책 2. 자바빈

매개변수가 많을 때 다른 대안으로는 기본 생성자를 사용해 인스턴스를 만들고, setter를 사용해서 필요한 필드만 설정할 수 있다.

// 인스턴스 생성
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(100);
cocaCola.setServings(4);
cocaCola.setCalories(140);
//...
클래스
// JavaBeans Pattern
public class NutritionFacts {
	private int servingSize; // 필수
	private int servings;
	private int calories;
	private int fat;
	//private int sodium;
 	//private int carbohydrate;
  
  	public NutritionFacts() {}
  	// Setters
  	public void setServingSize(int val) {servingSize = val;}
  	public void setServings(int val) {servings = val;}
  	public void setCalories(int val) {calories = val;}
  	// ...  
	
}

단점

  • 자바빈은 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 수 있다. 즉, 필수 필드를 채우지 않았거나 실수로 필요한 필드를 채우지 않았을 때 발생하는 오류를 디버그하기 어렵다.
  • Immutable(불변) 클래스에는 적용을 할 수 없다.

해결책 3. 빌더

다행히도 Telescoping constructor 패턴의 안정성과 Javabeans 패턴의 가독성의 장점을 합쳐놓은 Builder 패턴이 있다.

Builder 패턴은 객체를 바로 생성하지 않고 클라이언트는 빌더(생성자 또는 static 팩토리)에 필수인 매개변수를 주면서 호출해 Builder객체를 얻은 다음 빌더 객체가 제공하는 setter와 비슷한 메소드를 사용해서 Optional한 필드를 채워넣고 최종적으로 build() 메소드를 호출해 만들려는 개체를 생성한다.

// easy to write! easy to read!
NutriionFacts cocaCola = new NutritionFacts.Builder(500, 2) // 필수 필드
    .calories(100) // Optional 필드
    .sodium(20)
    .carbohydrate(20)
    .build();
클래스 (Validity 검사를 위해선 Builder 생성자와 메소드에서 검사하는 것이 좋다. 해당 코드에서는 생략했다.)
// Builder Pattern
public class NutritionFacts {
	private int servingSize; // 필수
	private int servings;
	private int calories;
	private int fat;
	//private int sodium;
 	//private int carbohydrate;
  
  	public static class Builder {
		// 필수 매개변수
  		private int servingSize;
  		private int servings;
  
  		// Optional 매개변수
  		private calories = 0;
  		private fat = 0;
  		// ...
  
  		public Builder calores(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;
  		//...
  	}
}

빌더의 생성자나 메소드에서 유효성을 확인할 수도 있고 여러 매개변수를 혼합해서 확인해야 하는 경우 build() 메소드에서 호출하는 생성자에서 할 수 있다. 빌더에서 매개변수를 객체로 복사해온 다음에 확인하고, 검증에 실패하면 IllegalArgumentException을 던지고 에러 메시지로 어떤 매개변수가 잘못 됐는지 알려줄 수 있다.

빌더 패턴은 클래스 계층 구조에 적합한 패턴이다. 추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에서는 추상 클래스를 상속받으며 각 하위 크래스용 빌더도 추상 빌도를 상속받악서 만들 수 있다.

NyPizza nyPizza = new NyPizza.Builder(SMALL)
    .addTopping(Pizza.Topping.SAUSAGE)
    .addTopping(Pizza.Topping.HAM)
    .build();
    
Calzone calzone = new Calzone.Builder()
    .addTopping(Pizza.Topping.HAM)
    .sauceInside()
    .build();
추상 클래스 Pizza
// Builder Pattern
public class Pizza {
	public enum Topping {
  		HAM, MUSHROOM, CHEESE, ONION
  	}
  
  	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 & Calzone
// 하위 클래스 1
public class NyPizza extends Pizza {
	public enum Topping {
  		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
  		abstract NyPizza build() {
			return new NyPizza(this);
		}
  		
		@Override
  		protected Builder self() {
			return this;
		}
  	}
  
  	private NyPizza(Builder builder) {
		super(builder);
  		size = builder.size();
	}
}
// 하위 클래스 2
public class Calzone extends Pizza {
	
	private final boolean sauceInside;
  
  	// 추상 클래스의 추상 빌더 클래스
  	public static class Builder extends Pizza.Builder<Builder> {
  		private final boolean sauceInside = false;
  
  		public sauceInside() { // 필수
			sauceInside = true;
			return this;
		}
  		
		@Override
  		abstract Calzone build() {
			return new Calzone(this);
		}
  		
		@Override
  		protected Builder self() {
			return this;
		}
  	}
  
  	private Calzone(Builder builder) {
		super(builder);
  		sauceInside = builder.sauceInside();
	}
}

이때 추상 빌더는 재귀적인 타입 매개변수를 사용하고 self()메소드를 사용해 self-type 개념을 모방할 수 있다. 하위 클래스에서는 build()메소드의 리턴 타입으로 해당 하위 클래스 타입을 리턴하는 Covariant 리턴 타이핑을 사용하면 사용자가 타입 캐스팅을 할 필요가 없다.

빌더는 가변인자 매개변수를 여러개 사용할 수 있는 장점도 있다. 또한 토핑 예제에서 본 것처럼 여러 메소드 호출을 통해 전달받은 매개변수를 모아 하나의 필드에 담는 것도 가능하다.

단점

  • 객체를 만들기 전 빌더를 만들어야 하는데 성능에 민감한 상황에서는 문제가 될 수 있다. 게다가 생성자를 사용하는 것보다 코드가 더 복잡하다. 따라서 빌더 패턴은 매개변수가 많거나 또는 앞으로 늘어날 가능성이 있는 경우에 사용하는 것이 좋다.

Ref.

Effective Java 3rd Edition by Joshua Bloch
백기선님 유튜브 동영상 및 자료
https://www.youtube.com/watch?v=X7RXP6EI-5E&list=PLOFN6hDJLxo0MYZd1z6GCaRdFWX3kTb6A&index=2

profile
Feedback is a gift

0개의 댓글