내가 자바로 웹 애플리케이션을 개발할때 인스턴스 생성에 대한 고민이 있었다.
Goods goods = new Goods();
goods.setGoodsId();
goods.setGoodsTitle();
goods.setGoodsCategory();
goods.set...
이와 같이 상품의 데이터가 많을때 ( null 이 허용되는 선택적 매개변수가 많을 때 ) 어떻게 대응하여 인스턴스를 생성할 것인가?
식품 포장의 영양정보를 표현하는 클래스를 생각해보자.
영양정보의 항목은 20개가 넘어간다고 해보자 그런데 대부분 제품은 이 선택 항목중 대다수 값이 0으로 표현된다고 하자
( 안쓰는 경우가 많다 라고 표현해보자 )
이런 클래스용 생성자는 어떤 모습일까 ?
기존에 개발자들은 점층적 생성자 패턴을 즐겨 사용했다고 한다.
/**
* 식품 포장의 영양정보를 표현하는 클래스
*/
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){
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
/**
* 지금은 매개변수가 6개 정보라 나빠보이지 않을 수 있다.
* 하지만 수가 더 늘어난다면 금세 걷잡을 수 없게 된다.
*/
@Test
public void 생성자_테스트() {
NutritionFacts nutritionFacts = new NutritionFacts(240, 8, 100, 0, 35, 27);
System.out.println(nutritionFacts);
}
점층적 생성자 패턴은 매개변수가 많이질수록 코드를 작성하거나 읽기 어려워 진다.
그렇다면 자바빈즈 패턴은 어떨까?
package designpattern.builder.beans;
/**
* 식품 포장의 영양정보를 표현하는 클래스
*/
public class NutritionFacts {
private int servingSize;
private int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
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;
}
@Override
public String toString() {
return "NutritionFacts{" + "servingSize=" + servingSize + ", servings=" + servings
+ ", calories=" + calories + ", fat=" + fat + ", sodium=" + sodium
+ ", carbohydrate=" + carbohydrate + '}';
}
}
@Test
public void 생성자_테스트_with_beans_pattern() {
designpattern.builder.beans.NutritionFacts nutritionFacts = new designpattern.builder.beans.NutritionFacts();
nutritionFacts.setServingSize(240);
nutritionFacts.setServings(8);
nutritionFacts.setCalories(100);
nutritionFacts.setFat(0);
nutritionFacts.setSodium(35);
nutritionFacts.setCarbohydrate(27);
System.out.println(nutritionFacts);
}
자바 빈즈 패턴은 심각한 문제를 가지고 있다.
- 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
- 일관성이 깨진 객체가 만들어지면 버그를 심은 코드가 될 수도 있다.
- 자바 빈즈 패턴에서는 클래스를 불변으로 만들 수 없다. 스레드 안정성을 얻으려면 프로그래머가 추가 작업을 해줘야만 한다.
그렇다면 어떻게 해야할까 ?
점층적 생성자 패턴의 안정성과 자바 빈즈패턴의 가독성을 겸비한 빌더 패턴을 사용하면 된다.
package designpattern.builder.builder;
/**
* 식품 포장의 영양정보를 표현하는 클래스
*/
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;
}
@Override
public String toString() {
return "NutritionFacts{" + "servingSize=" + servingSize + ", servings=" + servings
+ ", calories=" + calories + ", fat=" + fat + ", sodium=" + sodium
+ ", carbohydrate=" + carbohydrate + '}';
}
/**
* 이너 클래스 static builder
* <p>
* 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 게 보통이다.
* <p>
* final 로 선언할 수 있기 떄문에 lombok builder 와 차이가 있다.
*/
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;
// 필수 매개 변수는 final 선언과 생성자 선언을 해주어야 한다.
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
this.calories = val;
return this;
}
public Builder fat(int val) {
this.fat = val;
return this;
}
public Builder sodium(int val) {
this.sodium = val;
return this;
}
public Builder carbohydrate(int val) {
this.carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
/**
* NutritionFacts 는 불변이며, 모든 매개변수의 기본값들을 한곳에 모아 뒀다.
* <p>
* 이 클러이언트 코드는 쓰기 쉽고, 무엇보다도 읽기 쉽다.
*/
@Test
public void 생성자_테스트_with_builder_pattern() {
designpattern.builder.builder.NutritionFacts nutritionFacts =
new designpattern.builder.builder.NutritionFacts.Builder(240, 8).calories(100)
.fat(0).sodium(35).carbohydrate(27).build();
System.out.println(nutritionFacts);
}
심화 응용 한 빌더를 보겠다. -> effective java 3판
package designpattern.builder.builder;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
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();
}
}
package designpattern.builder.builder;
import java.util.Objects;
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;
}
@Override
public String toString() {
return "NyPizza{" + "size=" + size + ", toppings=" + toppings + '}';
}
}
public class NyPizzaTest {
@Test
public void 심화_builder_test() {
NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL).addTopping(Pizza.Topping.SAUSAGE).addTopping(
Pizza.Topping.MUSHROOM).build();
System.out.println(pizza);
}
}
'개발 > java' 카테고리의 다른 글
자바 직렬화에 대한 생각정리 (0) | 2020.07.26 |
---|---|
객체의 생성 (0) | 2020.07.09 |
Iterator design pattern (0) | 2020.05.28 |
JPA 2차 캐시란 (0) | 2020.04.26 |
JPA 락 (0) | 2020.04.25 |
댓글