들어가기전
이번 포스팅은 책에 나와있는 예제를 보고만 넘어 가기에는 빌더패턴을 왜 ? 꼭? 써야 되는지 정확하게 이해가 되지 않았기 때문에 조금 더 깊게 생각해볼 필요가 있을 것 같다. 빌더를 고려하라는 취지는 충분히 이해 했지만 만약에 실무에서 빌더를 이용해 설계를 해야된다면 ?? 지금은 절대로 설계가 불가능 하기 때문이다. 학습 목표는 빌더 패턴을 실무에 적용할 수 있을만한 필요성을 느끼고 상황에 맞게 잘 적용할 수 있도록 코드를 작성하며 느껴보기!!
학습 요약
- 빌터패턴(Builder pattern)을 사용해야 되는이유
- 점층적 생성자 패턴 (telescoping construcor pattern) 과 자바빈즈 패턴 (JavaBeans pattern )
- 빌터패턴 적용실습
- 계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴 적용실습 ( 소스 분석중 뇌정지옴 이 부분만 따로 포스팅 할 계획임 )
빌더패턴(Builder pattern)을 사용해야 되는이유
- 책의 내용에는 생성자의 매개변수가 늘어나면 개발자는 점층적 생성자 패턴과 자바빈즈 패턴 을 사용하는데 둘다 단점이 있기 때문에 이 둘의 장점만을 모아서 만든 빌더패턴을 사용하라고 하는데... 말로만 들어서는 뇌정지가 올것 같다.
백문이 불여일타! 코드로 알아보자
우선 책에 나오는 예제를 먼저 보자
점층적 생성자 패턴
(telescoping construcor pattern) 과 자바빈즈 패턴 (JavaBeans pattern )
점층적 생성자 패턴
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;
// 1단계 public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
// 2단계 public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
// 3단계 public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
// 4단계 public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
// 5단계 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 클래스를 이용해서 콜라 객체를 만들었고 콜라에 들가는 식품 정보를 넣어주고있다. public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}
|
cs |
장점 : 생성자 오버로드를 이용해서 2개부터 6개까지 필요에 따라서 생성 할 수 있다. 사용자 원하는 매개변수가 모두 포함된 생성자를 호출하면 된다.
단점 : 전달하는 파라미터가 늘어날 수록 점점 더 복잡하고 수 많은 생성자를 오버로드 하게 된다. 그리고 설계한 사람이 여러가지 업무적인 부분을 잘 판단해서 순서대로 파라미터를 나열했다고 하더라도 추후에 외부의 요인으로 파라미터 사용 순서라 바뀌게된다면 우리는 최소한의 파라미터를 사용하는 생성자가 아니고 최악의 경우 파라미터가 가장 많은 생성자를 호출하면서 처음과 맨 끝의 값만을 초기화 할 수도 있다. 또한 파라미터마다 모두 같은 이름이기 때문에 생성자를 호출할 때 가독성이 줄어들고 사용자를 고민을 하면서 사용하게 될 것이다.
그렇다면 이번에는 가독성이 좋고 파라미터가 늘어나도 상관 없도록 만들수 있는 패턴을 적용해 보자
자바빈즈 패턴
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() {}
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;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setFat(0);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
|
cs |
장점 : 앞서 작성한 점층적 생성장패턴의 단점이 더이상 보이지 않는다. 객체를 생성할 때 매개변수를 고려하지 않고 쉽게 만들 수 있고 사용하고 싶은 값만을 초기화 할 수 있어서 가독성에도 매우 좋아 보인다.
단점 : 객체 하나를 만드는데 메소드를 여러번 호출해야된다. 또한 원하는 값이 모두 set되기 전에는 일관성이 무너진 상태에 놓이게 된다. 또한 클래스를 불변 으로 만들 수 없기 때문에 켑슐화가 깨지게 된다.(조금 복잡하지만 freeze라고 해서 생성이 완료된 객체를 얼리는 작업을 하는 메소드를 만들어서 변하지 않도록 할 수 있지만 이또한 복잡한 작업이고 혹시라도 생성을 마치고 나서 freeze를 하지 않는다면 이또한 찾기 어려운 버그가 될 것으로 예측된다.)
빌터패턴 적용실습
자 이제 우리가 공부하려고 하는 빌터 패턴이 나올 차례다. 책에서는 위에서 설명한 패턴의 장점만을 가지고 만든어 놓은 패턴 이라고 한다.
빌더 패턴
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;
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.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;
// 필수 매개변수만을 담은 Builder 생성자
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// 선택한 메소드의 값을 set 하고 Builder 자신을 return 해주므로 메소드체이닝을 사용할 수 있다.
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;
}
// build() 호출로 최종 불변 객체를 얻는다.
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100) .sodium(35) .carbohydrate(30) .build(); }
}
|
cs |
장점 : 매개변수의 증가에 따른 가독성의 복잡성이 증가하지 않고 필수 값을 제외한 값은 필요에 따라서 메소드 체이닝을 이용해 언제든 유연하게 값을 설정할 수 있다.
빌더 패턴을 실무에 적용해서 만들고 싶어졌다. 내가 체이닝을 사용했던 적이 있는데 Spring Security 에서 configure메소드를 Override해서 화면마다 권한을 주고 필요에 따라서 로그인페이지 등 여러가지를 유연하게 사용할 수 있었다.
다만 내부적으로 빌더 패턴을 사용하는 것인지는 모르겠지만 체이닝을 사용하므로 편리했던 기억이 난다.
계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴 적용실습
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();
}
}
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() {
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;
}
public static void main(String[] args) {
NYPizza pizza = new NYPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM)
.sauceInside()
.build();
}
}
|
cs |
'Effective Java > 객체 생성과 파괴' 카테고리의 다른 글
#03 #02의 보충설명 - 계층적으로 설계된 클래스와 빌더패턴 (1) | 2021.05.31 |
---|---|
#01 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2021.05.15 |