서론
정적 팩터리 메서드와 생성자는 똑같은 제약사항이 하나 있다. 선택적 매개변수가 많을 경우 적절하게 대응하기 어렵다는 점이다.
개발자들은 이런 상황이 생기면 `점층적 생성자 패턴`을 주로 사용해왔다. 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자.... 등 다양하게 생성자를 늘려가는 방식을 `점증적 생성자 패턴`이라고 한다.
이런 생성자 패턴은 무엇이 문제가 있을까??
사용자가 원하지 않는 매개변수까지 포함해서 값을 넘겨줘야 한다는 것이다. 매개변수가 많지 않으면 어찌어찌 사용할 수 있지만 너무 많아진다면 상당히 곤란해질 것이다.
이런 점층적 생성자 패턴의 대안은 없는 것일까??
Effective Java에서는 2가지 대안으로 제시하고 있다.
자바빈즈 패턴
매개변수가 없는 생성자로 객체를 만든 후, setter 메서드를 호출해 원하는 매개변수의 값을 설장하는 방식이다.
밑의 코드는 식품의 영양정보 클래스에 `자바빈즈 패턴`을 적용시킨 코드이다.
public class JavaBeansPattern {
public static void main(String... args) {
JavaBeansNutritionFacts nutritionFacts = new JavaBeansNutritionFacts();
nutritionFacts.setServingsize(240);
nutritionFacts.setServings(10);
nutritionFacts.setCalories(30);
...
}
}
public class JavaBeansNutritionFacts {
private int servingSize = -1;
private int servings = -1;
private int caloreis = 0;
...
public JavaBeansNutritionFacts() { }
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
...
}
`자바빈즈 패턴`을 적용함으로써 `점층적 생성자 패턴`의 어떤 단점이 보안되었을까??
매개변수의 순서를 잘못입력할 일이 없어졌으며 여러개의 생성자를 만들 필요가 없어졌다.
하지만 `자바빈즈 패턴`은 큰 문제를 가지고 있다.
일단 객체를 하나 만들기 위해서는 여러 메서드를 호출해야 하며, 객체가 완전히 완성된 상태가 아닌 경우 일관성이 무너진 상태가 된다. 일관성이 무너진 상태가 되면 클래스를 불변으로 만들 수 없기 때문에 추가 문제가 발생할 여지가 있다.
빌더 패턴
개념
위의 문제들을 해결하고자 나온 패턴이 바로 `빌더 패턴`이다.
빌더 패턴은 점층적 생성자 패턴의 안정성(클래스 불변)과 자바빈즈 패턴의 가독성(읽기 쉬운 코드)을 합쳐저 만들어졌다고 할 수 있다.
클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자나 정적 펙터리 메서드를 호출해 빌더 객체를 얻는다. 이후 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다. 마지막으로 매개변수가 없는 build 메서드를 호출해 불변인 객체를 얻는다.
보통 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어둔다.
다음은 빌더 패턴을 만드는 코드이다.
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 {
// 필수 매개변수 -> final 예약어를 붙이게 된다면 필수 매개변수가 된다.
private final int servingSize;
private final int servings;
// 선택 매개변수
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int value) {
this.calories = value;
return this;
}
public Builder fat(int value) {
this.fat = value;
return this;
}
...
public NutritionFacts build() {
return new BuilderNutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}
}
기본적으로 NutritionFacts 클래스는 불변이며, 모든 매개변수의 기본값들을 한곳에 모아져있다. 빌더의 setter 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출이 가능해지는 구조를 가지게 된다.
이를 통해, 클라이언트는 다음과 같은 코드를 작성할 수 있다.
public class BuilderPattern {
public static void main(String... args) {
NutritionFacts nutritionFacts = new NutritionFacts
.Builder(240, 10)
.calories(80)
.fat(10)
.sodium(35)
.build();
}
}
만약 클라이언트가 잘못된 매개변수를 입력했다면 빌더의 생성자와 메서드에서 매개변수를 검사하고 builder 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사한다. 해당 부분에서 잘못된 점을 발견하면 `IllegalArgumentException`을 던지면 된다.
장점
위에서 잠깐 설명했듯이 생성할 객체는 불변이다. 그리고 모든 매개변수의 기본값들을 한곳에 모아둔 상태에서 빌더의 setter 메서드들은 빌더 자신을 반환하기 때문에 연쇄적 호출이 가능하다. 이를 `메서드 연쇄`라고 한다.
`메서드 연쇄`를 통해 코드를 쓰기 쉽고, 무엇보다 읽기가 쉽다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
이를 통해, 점층적 생성자 패턴, 자바빈즈 패턴의 장점들을 가지고 있다는 것을 확인할 수 있다.
또한 빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기에 좋다.
각 계층의 클래스에 관련 빌더를 멤버로 정의해보자. 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 한다.
public abstract class Hamburger {
private final Set<Patty> patties;
public enum Patty {PORK, BEEF, CHICKEN, FISH;}
// 추상 클래스는 추상 Builder를 가진다. 서브 클래스에서 이를 구체 Builder로 구현한다.
// 즉, 자기 자신의 하위 타입을 받는 Builder라고 말할 수 있다. (재귀적인 타입)
abstract static class Builder<T extends Builder<T>> {
EnumSet<Patty> patties = EnumSet.noneOf(Patty.class);
public T addPatty(Patty patty) {
patties.add(patty);
return self();
}
abstract Hamburger build();
// 자식 클래스에서 이 메서드를 overriding하여 this를 반환하도록 해야 한다.
// 'self-type' 개념을 사용해서 메서드 체이닝이 가능하게 함.
protected abstract T self();
}
// Item 50.
Hamburger(Builder<?> builder) {
patties = builder.patties.clone();
}
}
위의 코드는 추상 클래스로 선언된 Hamburger 클래스이다.
Hamburger의 Builder 클래스는 `재귀적 타입 한정`을 이용하는 제네릭 타입이다.
`Builder<T extends Builder<T>>`의 의미는 `Hamburger.Builder`의 타입 T는 자신을 상속받은 모든 Builder가 될 수 있다 라는 뜻이다.
즉, T로 표현된 타입이 Builder가 사용가능 해야 하므로, Builder를 구현한 클래스만 타입으로 사용해!!라고 강제하는 역할을 수행한다.
또한, 추상 메서드 self() 메서드를 통한 형 변환 없이 메서드 연쇄를 지원한다.
이 추상 클래스인 `Hamburger`의 하위타입인 `LotteHamburger`는 Hamburger의 Builder를 상속한 Builder를 구현함으로써, 상위타입에서 말한 `Builder를 상속하는 T`를 만족하였고, 이를 통해 추상클래스의 self 메서드를 오버라이딩이 가능해졌다. 즉, 추상클래스의 self()를 호출할 때, 상속받는 클래스에서 구현한 self()를 불러옴으로써, 자기자신의 Builder를 반환하게 되어 형변환을 하지 않고 메서드체이닝이 가능하게 된다.
public class LotteHamburger extends Hamburger {
private final boolean sauceInside;
public static class Builder extends Hamburger.Builder<Builder> {
private final boolean sauceInside;
private Builder(boolean sauceInside) {
this.sauceInside = sauceInside;
}
@Override
public LotteHamburger build() {
return new LotteHamburger(this);
}
@Override
protected Builder self() {
return this; // this를 반환해야 메서드체이팅이 가능하다.
}
}
public static Builder builder(boolean sauceInside) {
return new Builder(sauceInside);
}
LotteHamburger(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
public class Application {
public static void main(String... args) {
LotteHamburger lotteHamburger = new LotteHamburger.Builder(TRUE)
.addTopping(Hamburger.Patty.PORK)
.addTopping(Hamburger.Patty.CHICKEN)
.build();
}
}
정리
빌더 패턴은 앞에서 살펴본 2가지 패턴들 보다 유연하다는 장점을 가지고 있다. 하지만 Builder를 사용하기 위해 장황한 코드를 구현해야 한다는 단점 또한 존재한다. 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있으므로 애초부터 빌더로 설계를 하는 것이 더 좋을 수도 있다.
(참고로 Lombok을 쓰면 일정 부분 도움을 받을 수 있다.)
reference.
http://www.yes24.com/Product/Goods/65551284
https://jyami.tistory.com/94
'Programming > Effective Java' 카테고리의 다른 글
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.02.20 |
---|
댓글