FireDrago

객체를 생성하는 방법 (생성자, 정적메서드, 빌더패턴) 본문

프로그래밍/디자인패턴

객체를 생성하는 방법 (생성자, 정적메서드, 빌더패턴)

화이용 2024. 6. 5. 16:17
 
 

목차

1. 점층적 생성자 패턴

2. 자바빈즈 패턴

3. 빌더 패턴

4. 빌더 패턴의 계층적 활용

객체를 생성할때, 매개변수의 개수가 많다면 어떤 방법으로 객체를 생성하는 것이 좋을까?

 

객체를 생성할때 필수매개변수와 선택 매개변수가 있다면 가독성이 높고, 편리한 방법으로 객체를 생성할 수 없을까?

 

점층적 생성자 패턴

점층적 생성자 패턴은 필수 매개변수와 선택 매개변수가 있을때 생성자를 활용하여 객체를 생성하는 방법이다.

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); // 기본 칼로리 값을 0으로 설정
    }

    // 필수 + 선택1 (칼로리)
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    // 필수 + 선택2 (칼로리, 지방)
    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }


    // 필수 + 선택3 (칼로리, 지방, 나트륨)
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }


    // 필수 + 선택4 (칼로리, 지방, 나트륨, 탄수화물) 가독성이 떨어진다.
    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개 정도만 있는데, 이미 생성자가 엄청 많고, 가독성이 떨어진다. 

 

만약 실수로 생성자의 매개변수 순서를 바꿔서 만들었다고 생각해보자 어떤 생성자가 잘못되었는지 찾기 쉬울까?

 

또 이름을 줄 수 없다. 특별한 객체를 생성하고 싶어도 동일한 생성자를 호출 할 수 밖에 없다.

 

새로운 필드가 엔티티에 추가되는 경우, 생성자를 추가해야한다. 유지보수성이 낮을 수 밖에 없다.

 

반면 장점도 있는데, 생성자를 사용하기때문에 불변객체가 만들어진다. 

 

생성된 이후에도 안전성을 유지할 수 있다.

 

점층적 생성자 패턴

  • 가독성 저하: 많은 수의 생성자가 존재할 경우 코드의 가독성이 떨어진다.
  • 유지보수 어려움: 생성자의 매개변수 순서를 바꿀 경우 실수가 발생할 수 있다.
  • 이름 명시 불가: 각 생성자마다 이름을 줄 수 없기 때문에 코드의 명확성이 떨어진다
  • 확장성 문제: 새로운 매개변수를 추가할 때마다 새로운 생성자를 추가해야 한다.
  • 불변성 : 객체가 불변으로 만들어져 생성된 이후에도 안전성을 유지할 수 있다.

 

 

 

 

자바 빈즈 패턴

public class NutritionFacts {

    // final 사용불가, setter로 설정해야 하기때문
    private int servingSize = -1; // (ml, 1회 제공량) 필수; 기본값 없음
    private int servings = -1; // (회, 총 n회 제공량) 필수; 기본값 없음
    private int calories = 0; // (1회 제공량당 칼로리) 선택; 기본값
    private int fat = 0; // (g, 1회 제공량당 지방) 선택
    private int sodium = 0; // (mg/ 1회 제공량당 나트륨) 선택
    private int carbohydrate = 0; // (g, 1회 제공량 당 탄수화물) 선택

    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;}
}
public static void main(String[] args) {
    NutritionFacts nutritionFacts = new NutritionFacts();
    nutritionFacts.setServingSize(240);
    nutritionFacts.setServings(8);
    nutritionFacts.setCalories(100);
    nutritionFacts.setSodium(35);
    nutritionFacts.setCarbohydrate(27);
}

자바 빈즈 패턴은 가독성 문제를 말끔히 해결한다. 기본값을 가지는 필드로 일단 객체를 생성하고,

 

setter를 사용하여 필드값을 초기화 한다. 생성자만큼 복잡하지 않고, 깔끔하게 객체를 생성할 수 있다.

 

하지만 자바빈즈 패턴은 사용을 지양해야하는 안티패턴이다.

 

setter로 필드를 초기화 해주기 전까지 인스턴스는 안전성이 없는 불안정한 상태에 놓이게 된다.

 

불안정한 객체를 호출하거나 사용했다가는 버그가 발생할 것이다. 

 

불변객체로 생성할 수 없기때문에 초기화가 완료된 이후에도 언제든 값이 변경될 위험이 있다.

 

자바빈즈 패턴 (쓰지말자)

 

  • 가독성 향상: 생성자보다 코드가 더 읽기 쉽고 직관적이다.
  • 유연성: 객체 생성 후에 필드를 개별적으로 설정할 수 있다.
  • 불안정성 : setter를 사용하여 모든 필드를 초기화하기 전까지 객체는 불안정한 상태에 놓인다.
  • 불변 객체 불가: 초기화가 이후에도 언제든 값이 변경될 수 있어 객체의 불변성을 유지할 수 없다.

 

 

 

 

빌더 패턴

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, 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;
    }

    public static NutritionFactsBuilder builder() {
        return new NutritionFactsBuilder();
    }

    public static class NutritionFactsBuilder {
        private int servingSize;
        private int servings;
        private int calories;
        private int fat;
        private int sodium;
        private int carbohydrate;

        NutritionFactsBuilder() {
        }

        public NutritionFactsBuilder servingSize(int servingSize) {
            this.servingSize = servingSize;
            return this;
        }

        public NutritionFactsBuilder servings(int servings) {
            this.servings = servings;
            return this;
        }

        public NutritionFactsBuilder calories(int calories) {
            this.calories = calories;
            return this;
        }

        public NutritionFactsBuilder fat(int fat) {
            this.fat = fat;
            return this;
        }

        public NutritionFactsBuilder sodium(int sodium) {
            this.sodium = sodium;
            return this;
        }

        public NutritionFactsBuilder carbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this.servingSize, this.servings, this.calories, this.fat, this.sodium, this.carbohydrate);
        }
    }
}

빌더 패턴은 필수 매개변수 만으로 생성자, 정적 팩토리 메서드를 호출해 빌더 객체를 얻는다.

 

빌더 객체가 제공하는 세터 메서드 들로 원하는 선택 매개변수들을 설정한다. 이 메서드들은 연쇄호출될 수 있다.

 

마지막으로 매개변수가 없는 build 메서드를 호출하여 필요한 객체를 얻는다.

 

빌더 클래스는 생성 클래스에 정적 멤버 클래스로 만들어 둔다.

 

빌더패턴을 사용하면 가독성이 좋고, 불변성이 보장된다.

 

빌더패턴

 

  • 가독성 향상: 객체 생성 시 각 매개변수의 이름을 명시할 수 있어 코드가 더 읽기 쉽고 직관적이다.
  • 유연성: 필요한 매개변수만 선택적으로 설정할 수 있어, 다양한 형태의 객체를 쉽게 생성할 수 있다.
  • 불변 객체 생성: 객체가 완전히 생성된 후에는 불변성을 유지할 수 있어 안전하다.
  • 확장성: 새로운 필드를 추가할 때 기존 코드를 변경할 필요 없이 빌더에만 필드를 추가하면 된다.
  • 순서 무관: 매개변수의 순서에 관계없이 객체를 생성할 수 있어 실수로 인한 버그를 줄일 수 있다.
  • 추가 코드 필요: 빌더 클래스를 정의하고 메서드를 작성해야 하기 때문에 코드가 길어질 수 있다.
  • 메모리 사용: 빌더 객체를 생성하기 때문에 추가적인 메모리 사용이 있을 수 있다.

매개변수가 많다면 (4개 이상), 빌더 패턴을 고려해보자!

 

 

 

빌더  패턴의 계층적 활용

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();

        // 하위 클래스는 이 메서드를 오버라이딩(overriding) 하여
        // 'this'를 반환하도록 해야 한다.
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // 아이템 50 참조
    }
}

빌더 패턴은 계층적으로 설계된 클래스에 사용하기 좋다.

 

Builder<T extends Builder<T>> 재귀적 타입 한정을 사용한다. Builder 자신과 자손만을 타입으로 한정하기 위한 제네릭

 

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;
    }

}

Pizza 를 상속받은 NyPizza 클래스이다.

 

각 하위 클래스가 정의한 build 메서드는 구체 하위 클래스를 반환하도록 만들 수 있다.

abstract Pizza build();

================오버라이딩=============

@Override
 public NyPizza build() {
     return new NyPizza(this);
 }

 

public static void main(String[] args) {
	NyPizza pizza = new NyPizza.builer(SMALL)
                 .addTopping(SAUSAGE)
                 .addTopping(ONION)
                 .build();
}

빌더패턴을 이용하여 객체를 생성하는 클라이언트 코드이다.