2020. 8. 10. 13:06ㆍ기술 서적/Effective Java 3∕E
Effective Java 3/E
정적 팩터리 메서드와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 공통점을 갖는다.
선택적 매개변수가 많을 때 활용할 수 있는 방법은 아래와 같다>
- 점층적 생성자 패턴(telescoping constructor pattern)
- 자바빈즈 패턴(JavaBeans pattern)
- 빌더 패턴(Builder pattern)
점층적 생성자 패턴(telescoping constructor pattern)
필수 매개변수만 받는 생성자,
필수 매개변수와 선택적 매개변수 1개를 받는 생성자,
필수 매개변수와 선택적 매개변수를 2개까지 받는 생성자,
… 이러한 형태로 선택 매개변수를 전부 다 받는 생성자까지 늘려가는 방식
public class NutritionFacts {
private final int servingSize; // (ml, 1회 제공량) 필수
private final int servings; // (회, 총 n회 제공량) 필수
private final int calories; // (1회 제공량당) 선택
private final int fat; // (g/1회 제공량) 선택
private final int sodium; // (mg/1회 제공량) 선택
private final int carbohydrate; // (g/1회 제공량) 선택
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;
}
}
식품 포장의 영양정보를 표현하는 클래스를 점층적 생성자 패턴으로 만든 예시.
이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은것을 골라 호출하면 된다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
인스턴스 생성 시 탄수화물(carbohydrate
)값을 지정해서 만들려면, 탄수화물 값을 매개변수로 받는 생성자 중 가장 짧은 것인 마지막 생성자를 호출해야한다.
이 때, 지방(fat
)은 기본값인 0이지만, 생성자에서 매개변수로 받고있기 때문에 0을 넘겨줘야만 한다.
만약 선택 매개변수 개수가 아주 많아진다면 클라이언트 코드를 작성할 때 각 값의 의미가 무엇인지 헷갈릴 수 있고, 매개변수가 몇 개 인지도 주의해서 세어야 할 것이다.
타입이 같은 매개변수를 연속적으로 받을 경우 버그를 잡기 어려워 질 수도 있다.
대신, 생성자에서 매개변수들이 유효한지 확인하면 이후에는 객체가 일관성을 유지할 수 있다.
자바빈즈 패턴(JavaBeans Pattern)
매개변수가 없는 생성자로 객체를 만든 후, setter
메서드들을 호출 원하는 매개변수의 값을 설정하는 방식
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() {
}
// setter
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(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);;
cocaCola.setCarbohydrate(27);
점층적 생성자 패턴과 달리, 기본값으로 설정할 매개변수인 지방(fat
)을 지정하지 않아줘도 된다.
하지만, 객체 하나를 만들려면 setter
메서드를 여러 개 호출해야 하고
객체가 완전히 생성되기 전까지는 (필요한 매개변수들이 setter
메서드를 통해 모두 세팅되기 전까지는) 일관성(consistency)이 무너진상태에 놓이게 된다. 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며, 스레드 안정성을 얻으려면 프로그래머가 추가 작업을 해야한다.
이를 보완하기 위해 생성이 끝난 객체를 수동으로 얼리고(freezing) 얼리기 전에는 사용할 수 없도록 하는 방법도 있지만 실전에서는 거의 쓰이지 않는다. 다루기 어렵고, 객체 사용 전 프로그래머가 freeze
메서드를 확실히 호출했는지를 컴파일러가 보증 할 방법이 없어서 런타임 오류에 취약하기 때문이다.
빌더 패턴(Builder pattern)
생성자 (혹은 정적 팩터리) 메서드를 호출해 필수 매개변수만으로 빌더 객체를 얻고
빌더 객체가 제공하는 일종의 setter
메서드들로 원하는 선택 매개변수들을 설정한 뒤
마지막으로 매개변수가 없는 build
메서드를 호출해 필요한 객체(보통은 불변)를 얻는 방식
public class NutritionFacts {
// 빌더 패턴(Builder pattern)
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 NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
빌더의 세터 메서드들(calories
, fat
, sodium
, carbohydrate
)은 빌더 자신을 반환(return this
)하기 때문에 연쇄적으로 호출이 가능하다.
이런 방식을 플루언트 API(fluent API) 혹은 메서드 연쇄(method chaining)라 한다.
매개변수를 모두 설정 한 후 build
메서드를 호출해야 객체가 생성되기 때문에 NutritionFacts
클래스는 불변이다.
빌더 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수(named optional parameters)를 흉내낸 것이다. (C#에도 있다.)
위의 코드에는 매개변수의 유효성 검사가 생략되었다. 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고 build()
메서드가 호출하는 생성자에서 여러 매개변수에 따른 불변식(invariant)을 검사하면 된다. 공격에 대비해 불변식을 보장하려면, 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야한다. 잘못된 점을 발견하면 IllegalArgumentException
으로 오류 내용에 대한 메시지를 던지면 된다.
계층적으로 설계된 클래스에 빌더 패턴 적용하기
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 build();
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
Builder
에 시뮬레이트한 셀프 타입(simulated self-type) 관용구인 self()
추상 메서드를 넣어 자식 클래스에서 형변환 하지 않고도 메서드 체이닝을 할 수 있도록 지원했다.
public class NyPizza extends Pizza {
public enum Size {
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
public NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
this.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;
}
}
공변 반환 타이핑 (convariant return typing)
공변(convariant)은 함께 변한다는 의미
자식 클래스의 메서드가 부모 클래스 메서드 정의에서 지정한 리턴 타입(Pizza
)이 아닌, 해당 타입의 하위 타입 (NyPizza
, Calzone
)을 반환하는 기능
이를 사용하면 클라이언트가 형변환에 신경쓰지않고 빌더를 사용할 수 있다.
Pizza.Builder.build()
는 Pizza
를 반환하지만,
NyPizza.Builder.build()
는 NyPizza
를 반환하고, Calzone.Builder.build()
는 Calzone
를 반환한다.
클래스 다이어그램으로 표현하면 위와 같다.
NyPizza
와 Calzone
가 추상 클래스인 Pizza
를 상속받는다.
그리고 NyPizza.Builder
와 Calzone.Builder
는 부모 클래스로 Pizza.Builder
를 상속받는다.
package item02;
import static item02.Pizza.Topping.*;
import static item02.NyPizza.Size.*;
public class MakingPizza {
public static void main(String[] args) {
NyPizza nyPizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM)
.sauceInside().build();
System.out.println(nyPizza);
System.out.println(calzone);
}
}
다이어그램에서 ~toppings
를 보면 여러개 있을 수도, 아예 없을 수도 있는(0..*
) 가변 인수이다. (variable arguments)
addTopping()
은 여러 번 호출될 수 있고, 호출 될 때 마다 매개변수를 하나의 필드인 EnumSet<Topping> toppings
에 모아놓는다.
다른 방법으로는 각각을 적절한 메서드로 나눠 선언하면 된다. (addToppingHam, addToppingMushroom, ... 이런식으로 메서드를 개별로 추가하면 된다는 말인가?)
참고 자료
'기술 서적 > Effective Java 3∕E' 카테고리의 다른 글
[이펙티브 자바] item 3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2020.08.29 |
---|---|
[이펙티브 자바] item 1 - 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2020.08.08 |
[이펙티브 자바] (0) | 2020.08.08 |