Study

생성자 매개변수가 많은 경우에 빌더 사용을 고려

부산대보금자리 2022. 11. 3. 15:04

생성시 매개변수가 많은 경우 어떤 방법을 통해 생성할 수 있을지 고려해보자.

 

해결책 1. 생성자

기본적인 방법으로 일일이 입력하여 객체를 생성한다.

NutritionFacts A = new NutritionFacts(100,10,10,0,100);

 

이러한 경우 필요없는 매개변수를 넘겨야 하는 경우가 존재하는데 이럴 시 Flag를 넣거나 0과 같은 기본값을 통해 로직을 구현한다.

따라서 작성하기도 어렵고 읽기에 좋지 않은 코드가 된다.

 

해결책 2. 자바빈

아무런 매개변수를 받지 않는 생성자를 사용해서 인스턴스를 만들고, 세터를 사용해서 필요한 필드만 설정한다.

이 방법의 단점은 최종적인 인스턴스를 만들기까지 여러번의 호출을 거쳐야 하기 때문에 자바빈이 중간에 사용되는 경우 안정적이지 않은 상태로 사용될 여지가 있다. 또한 불변 클래스로 만들지 못하고 쓰레드 간 공유가능하기 때문에 안정성을 보장하려면 추가적인 수고(locking)이 필요하다.

 

해결책 3. 빌더

앞서 소개한 생성자 방법의 안정성과 자바빈의 가독성을 모두 취할 수 있는 대안이 바로 빌더 패턴이다.

빌더 패턴은 만들려는 객체를 바로 만들지 않고 클라이언트는 빌더에 필수 매개변수를 주면서 호출하여 Builder 객체를 얻은 다음

빌더 객체가 제공하는 세터와 비슷한 메소드를 사용해서 부가적인 필드를 채워넣고 최종적으로 build라는 메소드를 호출하여 만들려는 객체를 생성한다.

 

NutritionFacts A = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

빌더의 생성자나 메소드에서 유효성 확인을 할 수도 있고 여러 매개변수를 혼합해서 확인해야 하는 경우에는 build 메소드에서 호출하는 생성자에서 할 수 있다.

 

package org.example;

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEEPER, 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(); // `Convariant 리턴 타입`을 위한 준비작업

        protected abstract T self(); // `self-type` 개념을 사용해서 메소드 체이닝이 가능케 함
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }

}

 

 

package org.example;


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

 

이때 추상 빌더는 재귀적인 타입 매개변수를 사용하고 self라는 메소드를 사용해 self-type 개념을 모방할 수 있다. 하위 클래스에서는 build 메소드의 리턴 타입으로 해당 하위 클래스 타입을 리턴하는 Covariant 리턴 타이핑을 사용하면 클라이언트 코드에서 타입 캐스팅을 할 필요가 없어진다.

NyPizza nyPizza = new NyPizza.Builder(SMALL)
    .addTopping(Pizza.Topping.SAUSAGE)
    .addTopping(Pizza.Topping.ONION)
    .build();

Calzone calzone = new Calzone.Builder()
    .addTopping(Pizza.Topping.HAM)
    .sauceInde()
    .build();

빌더는 가변 인자 (vargars) 매개변수를 여러개 사용할 수 있다는 소소한 장점도 있다. (생성자나 팩토리는 가변인자를 맨 마지막 매개변수에 한번밖에 못쓰니까요.) 또한 토핑 예제에서 본것처럼 여러 메소드 호출을 통해 전달받은 매개변수를 모아 하나의 필드에 담는 것도 가능하다.

빌더는 꽤 유연해서 빌더 하나로 여러 객체를 생성할 수도 있고 매번 생성하는 객체를 조금씩 변화를 줄 수도 있다. 만드는 객체에 시리얼 번호를 증가하는 식으로.

단점으로는 객체를 만들기 전에 먼저 빌더를 만들어야 하는데 성능에 민감한 상황에서는 그점이 문제가 될 수도 있다. 그리고 생성자를 사용하는 것보다 코드가 더 장황하다. 따라서 빌더 패턴은 매개변수가 많거나(4개 이상?) 또는 앞으로 늘어날 가능성이 있는 경우에 사용하는것이 좋다.

 

'Study' 카테고리의 다른 글

Static 클래스의 noninstantiability  (0) 2022.11.08
싱글톤 객체 생성  (0) 2022.11.08
생성자 대신 Static 팩토리 메소드의 사용  (0) 2022.11.02
정보처리기사 5과목 오답  (0) 2022.02.23
정보처리기사 4과목 오답  (0) 2022.02.23