이펙티브 자바 책을 읽으면서 새로 알게 된 부분을 기록한 것입니다.
생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩토리와 생성자가 가진 제약 - 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이다.
- 선택적 매개변수가 많을 경우 사용할 수 있는 몇 가지 패턴을 보여준다.
예시) 식품포장의 영양정보를 표현하는 클래스
- 1회 내용량, 총 n회 제공량, 제공량당 칼로리같은 필수 항목 몇개 + 총 지방, 트랜스지방, 포화지방, 콜레스트롤, 탄수화물 등 20개가 넘는 선택항목으로 이루어짐. 선택항목 중 대다수 값이 그냥 0이다.
점층적 생성자 패턴
- 이럴 때 프로그래머들은 점층적 생성자 패턴
telescoping constructor pattern
을 즐겨 사용했다. - 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수1개를 받는 생성자부터 시작해서 선택 매개변수 전부를 받는 생성자까지 늘려가는 방식이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class NutritionFacts {
private final int servingSize; // 1회 제공량
private final int servings; // 총 n회 제공량
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, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
- 단점 : 점층적 생성자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
자바빈즈 패턴
JavaBeans pattern
: 매개변수가 없는 생성자로 객체를 만든 후, setter 메소드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NutritionFacts {
// 기본값이 있다면 기본값으로 초기화된다.
private final int servingSize = -1; // 기본값 없음
private final int servings = -1;
private final int calories = 0;
private final int fat = 0;
private final int sodium = 0;
private final int carbohydrate = 0;
public NutritionFacts() { }
// setter들
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}
- 장점 : 점층적 생성자 패턴보다 인스턴스를 만들기 쉽고, 읽기 쉬운 코드가 되었다
- 단점 : 객체 하나를 만들려면 메소드를 여러 개 호출해야한다. 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태가 된다. -> 클래스를 불변으로 만들 수 없다.
빌더 패턴
-
Builder pattern
: 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비했다. -
작동 방식
- 클라이언트가 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
- 그 다음 빌더 객체가 제공하는 일종의 세터 메소드들로 원하는 선택 매개변수들을 설정한다.
- 매개변수가 없는 build 메소드를 호출해 우리에게 필요한 객체를 얻는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 final int calories = 0;
private final int fat = 0;
private final int sodium = 0;
private final 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클래스는 불변
- 모든 매개변수의 기본값들을 한 곳에 모아두었다.
- 빌더의 setter 메소드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출될 수 있다.
플루언트 API
,method chaining
: 메소드 호출이 흐르듯 연결된다는 뜻
이 클래스를 사용하는 클라이언트 코드의 형태
1
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
-
빌더 패턴은 (파이썬과 스칼라에 있는) 명명된 선택적 매개변수
named optional parameters
를 흉내낸 것이다. -
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다.
- 각 계층의 클래스에 관련 빌더를 멤버로 정의하자
- 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 한다.
계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
// 하위 클래스는 이 메소드를 오버라이딩하여 this를 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
- Pizza.Builder클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다.
- 여기에 추상 메소드인 self를 더해 하위 클래스에서는 형변환을 하지 않고도 메소드 연쇄를 지원할 수 있다.
- self타입이 없는 자바를 위한 이러한 우회 방법을
simulated self-type
관용구라 한다.
pizza의 하위 클래스 1 : 뉴욕 피자 -> size를 필수 매개변수로 받음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
}
- NyPizza.Builder는 NyPizza를 반환함.
pizza의 하위 클래스 2 : 칼초네 피자 -> sauceInside를 필수 매개변수로 받음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Calzone extends Pizza {
public final boolean sauceInside;
public static class Builder extends Pizza.Builder<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;
}
}
- 각 하위 클래스의 빌더가 정의한 build 메소드는 해당하는 구체 하위 클래스를 반환하도록 선언한다.
- Calzone.Builder는 Calzone를 반환한다는 뜻이다.
- 하위 클래스의 메소드가 상위 클래스의 메소드가 정의한 반환타입이 아닌, 그 하위 타입을 반환하는 기능 : ` 공변반환 타이핑``covariant return typing `
- 클라이언트측 코드 예시
1
2
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();
-
장점
- 빌더를 사용하면 가변인수
varargs
매개변수를 여러 개 사용할 수 있다. 각각을 적절한 메소드로 나눠 선언하면 된다. - 메소드를 여러 번 호출하도록 하고, 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수도 있다.
- 빌더 패턴은 유연하다. 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다. 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.
- 빌더를 사용하면 가변인수
-
빌더 패턴의 단점
- 객체를 만들려면 빌더부터 만들어야 한다.
- 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.
- 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되너야 값어치를 한다. (API는 시간이 지날수록 매개변수가 많아지는 경향이 있음을 명심하자)
불변이란 ?
immutable
이란, 어떠한 변경도 허용하지 않는다는 뜻이다.
주로 변경을 허용하는 mutable
객체와 구분하는 용도로 쓰인다. 대표적으로 String 객체는 한 번 만들어진 값이 절대 변하지 않는 불변객체다.
불변식
invariant
란 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 의미한다.
변경을 허용할 수는 있으나 주어진 조건 내에서만 허용한다는 뜻이다. 예를 들어, 리스트의 크기는 반드시 0 이상이어야 하니 만약 한 순간이라도 음수값이 된다면 불변식이 깨진 것이다.
참고자료
<Effective Java>