Study/Effective-Java

[item#02] 정적 팩터리와 생성자에 선택적 매개변수가 많을 때 고려할 수 있는 방안

hongeeii 2023. 11. 27.
728x90
반응형

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

package com.example.demo.chaper01.item02;

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

위 패턴의 문제점은 코드를 작성하기나 읽기 어렵다.

대안 2. 자바 빈즈 패턴

// 기본값이 있다면 기본값으로 초기화된다.
public class NutritionFacts {
	private int servingSize = -1;
	private int servings = -1;
	private int calories;
	private int fat;
	private int sodium;
	private int carbohydrate;
	
	public int getServingSize() {
		return servingSize;
	}
	public void setServingSize(int servingSize) {
		this.servingSize = servingSize;
	}
	public int getServings() {
		return servings;
	}
	public void setServings(int servings) {
		this.servings = servings;
	}
	public int getCalories() {
		return calories;
	}
	public void setCalories(int calories) {
		this.calories = calories;
	}
	public int getFat() {
		return fat;
	}
	public void setFat(int fat) {
		this.fat = fat;
	}
	public int getSodium() {
		return sodium;
	}
	public void setSodium(int sodium) {
		this.sodium = sodium;
	}
	public int getCarbohydrate() {
		return carbohydrate;
	}
	public void setCarbohydrate(int carbohydrate) {
		this.carbohydrate = carbohydrate;
	}
}

public static void main(String[] args) {
		NutritionFacts facts = new NutritionFacts();
		facts.setServings(240);
	}

장점 : 객체 생성이 간단해진다.
단점 :

  1. 필수적으로 필요로 하는 필수값들이 세팅이 안된상태로 그냥 사용이 될 수 있다. (일관성이 무너진 상태가 될 수 있다.)
  2. 어디까지 setting을 해주어야 하는지 문서화가 필요하다.
  3. 불변 객체로 만들 수 없다.

두 가지 방법을 혼용해서 필수적인 필드들은 생성자로 넘겨받도록 강제성을 주고, Optional한 필드들은 setter로 설정을 하도록한다.
이 방법도 여전히 하나의 단점이 있는데, 불변 객체로(immutable) 만들기 어렵다.

불변객체로 만들려면 setter를 사용하지 말아야한다.
왜? 객체의 일관성이 깨지기 때문에 => freezing이라는 기술도 사용할 수 있지만, 실전에서는 거의 쓰이지 않음.

대안 3. 권장하는 방법 : 빌더 패턴

  • 플루언트 API 또는 메서드 체이닝을 한다.
  • 계층적으로 설계된 클래스와 함께 사용하기 좋다.
  • 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

단점:

  • 반드시 좋은건 아니다.
  • immutable로 만들고 싶을 때가 있을 수 있음
  • 코드가 굉장히 많다. (중복 필드 포함) => lombok의 @Builder를 사용하면 Builder를 알아서 만들어줌.
  •  
package com.example.demo.chaper01.item02;

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 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) {
			// 자바 빈즈는 아니라 set메서드는 아니지만 setter와 동일한 역할
			// setter는 return type void
			// Builder는 return type 자기 자신
			// 이 때문에 Fluent API 또는 메서드 연쇄(method chaining)이 가능
			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;
	}

	public static void main(String[] args) {
		NutritionFacts facts = new NutritionFacts.Builder(240, 8) 
				.calories(2)
				.carbohydrate(5)
				.sodium(23)
				.build();
	}
}

@Builder를 사용한 예시
장점 : 간결하다.
단점 :

  1. 모든 파라미터를 받는 생성자가 기본으로 생긴다. (@AllArgsConstructor(access = AccessLevel.PRIVATE))를 사용하면 외부에서는 생성자를 못쓴다.(내부적으로 Builder만 사용가능) 
  2. 필수 값을 지정해 줄 수 없다. => 위 코드를 사용해야함(Builder를 직접 만드는 패턴)
import lombok.Builder;

@Builder(builderClassName = "Builder")
@AllArgsConstructor(access = AccessLevel.PRIVATE)
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 static void main(String[] args) {
		NutritionFacts facts = new NutritionFactsBuilder()
				.servingSize(100)
				.servings(10)
				.build();
				
		// 빌더에 name을 주면 아래와 같이 사용가능 
		NutritionFacts facts = new NutritionFacts.BuilderNameTest().build();
	}
}

 

!! Annotation Processor

Annotation자체만으론 주석에 가까움.

 

컴파일 단계에서 Annotation에 정의된 일렬의 프로세스를 동작하게 하는 것을 의미.
컴파일 단계에서 실행되기 때문에, 빌드 단계에서 에러를 출력하게 할 수 있고, 소스코드 및 바이트 코드를 생성할 수도 있다.
https://roadj.tistory.com/9

 

애노테이션 프로세서(Annotation processor)

Annotation Processor란? Annotation Processor는 컴파일 단계에서 Annotation에 정의된 일렬의 프로세스를 동작하게 하는 것을 의미합니다. 컴파일 단계에서 실행되기 때문에, 빌드 단계에서 에러를 출력하게

roadj.tistory.com

 

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>> { // Builder자신의 하위 클래스를 받도록 재귀적인 타입 제한
		EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

		public T addTopping(Topping topping) {
			toppings.add(Objects.requireNonNull(topping));
			return self();
		}
		
		public Builder<T> addTopping2(Topping topping) {
			toppings.add(Objects.requireNonNull(topping));
//			return self();
			// 왜 this를 return 시키면 안되는 가? 
			// 하위 클래스의 빌더들은 자기자신빌더를 리턴해야한다.
			// 하위 타입을 리턴할수 있도록 self()를 호출하도록 한 것.
			return this;
		}

		abstract Pizza build();

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

	}

	Pizza(Builder<?> builder) {
		toppings = builder.toppings.clone();
	}
}
public class Calzone extends Pizza {
	private final boolean sauceInside;

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

		public Builder sauceInside() {
			sauceInside = true;
			return this;
		}

		@Override
		Calzone build() {
			return new Calzone(this);
		}

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

	}

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

}

addTopping2 메서드에서 return this를 하면 하위 클래스를 build할 때 타입 캐스팅을 해야하므로 불편함.

public class PizzaTest {
	public static void main(String[] args) {
		NyPizza nyPizza2 = new NyPizza.Builder(NyPizza.Size.SMALL)
				.addTopping(Pizza.Topping.HAM)
				.addTopping(Pizza.Topping.MUSHROOM).build();
		
		NyPizza nyPizza = (NyPizza) new NyPizza.Builder(NyPizza.Size.SMALL) // 타입 캐스팅해야함.
		.addTopping2(Pizza.Topping.HAM)
		.addTopping2(Pizza.Topping.MUSHROOM).build();
		
	}
}

self()를 사용하여 형변환을 줄일 수 있고 BuilderFactory 계층 구조를 재활용 할 수 있다라는 걸 책에서 말하고 있다.

728x90
반응형

추천 글