[Effective Java] Item2. 생성자에 매개변수가 많다면 빌더를 고려하라

최강일·2024년 3월 23일
0

Effective Java

목록 보기
1/9

Item1에서 클래스에서 인스턴스를 생성하는 생성자와 static 팩토리 메서드를 배웠다.

그러나 두 방법에는 똑같은 문제가 하나 있다.
"선택적 매개변수가 많을 때 적절히 대응하기 어렵다."라는 것이다.
이제 선택 매개변수가 많은 상황에서 대처법을 배워본다.

  • 필수 매개변수 - 인스턴스 생성에 꼭 필요한 변수
  • 선택 매개변수 - 인스턴스 생성에 있어도 되고 없어도 되는 변수

점층적 생성자 패턴

전통적으로 사용해오던 방식으로 이해가 쉬울 것이다.

필수 매개변수만 가진 생성자
필수 매개변수 + 선택 매개변수1개 를 가진 생성자
필수 매개변수 + 선택 매개변수2개 를 가진 생성자
...
....
.....
위와 같이 매개변수를 점점 늘려가며 조합에 대한 생성자를 생성하는 것

문제점

하지만 필드가 늘어날수록 각 매개변수의 의미파악이 힘들어지고, 원하는 조합의 생성자를 찾기 힘들어진다.
또한 순서, 갯수 등을 틀리고 컴파일러에 걸리지 않으면 런타임 에러를 발생시킬 수 있다.

자바 빈즈 패턴

점층적 생성자 패턴은 매개변수의 조합을 맞추는 것이 포인트였다면,
자바빈즈 패턴은 객체를 만든 후 매개변수를 주입하는 것이 포인트다.
점층적 생성자 패턴보다 실수할 일이 줄어들었다.

  • 매개변수가 없는 생성자를 사용
  • 생성자를 통해 객체를 생성
  • setter 메서드를 통해 매개변수들을 하나씩 주입
Car c = new Car();
car.setXXX(...);
car.setXXX(...);
car.setXXX(...);
car.setXXX(...);
..
...
....

문제점

점층적 생성자 패턴의 경우 적절한 생성자 검증이 귀찮긴 하지만, 매개변수들을 이용해 한번에 생성하므로 그 객체의 일관성이 유지된다.

하지만 자바 빈즈 패턴의 경우 시스템적으로 문제를 가진다.
객체를 일단 생성해놓고 변수를 하나하나 주입한다.
객체 생성은 되었어도 매개변수 주입 과정이 끝나지 않으면, 객체는 일관성이 무너진 상태가 된다.
쉽게 말해, 매개변수가 하나 추가 될 때마다 인스턴스가 변한다는 말이다.

더 자세히

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

빌더 패턴

점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴이다.

사용법

  • 사용자가 필수 파라미터만으로 생성자를 호출해 빌더 객체를 얻는다.
  • 빌더 객체가 제공하는 setter 메서드들로 원하는 선택 매개변수들을 설정한다.
  • build() 메서드를 호출해 객체를 얻는다.
  • Builder는 생성할 클래스 안에 static 멤버 클래스로 만들어두는게 보통이다.
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 {
        // 필수 매개변수
        private final int servingSize;
        private final int servings;

        // 선택 매개변수 - 기본값으로 초기화한다.
        private int calories      = 0;
        private int fat           = 0;
        private int sodium        = 0;
        private int carbohydrate  = 0;

        public Builder(int servingSize, int servings){
            this.servingSize = servingSize;
            this.servings    = servings;
        }

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

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
   
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts
  					.Builder(240, 8)
  					.calories(100)
  					.sodium(35)
  					.carbohydrate(27)
  					.build();
    }
}

NutritionFacts 라는 클래스는 불변(Immutable)이 된다.
빌더의 setter 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.

클라이언트 코드는 쓰기 쉽고, 읽기 쉽다.

빌더 패턴의 유효성 체크

  • 빌더의 생성자와 메서드에서 매개변수를 검사
  • build 메서드가 호출하는 생성자에서 여러 매개변수에 대한 불변식 검사
  • 검사를 통해 구체적으로 어떤 매개변수가 잘못되었는지를 알려주는 IllegalArgumentException을 이용

불변 이란?

  • Immutable 혹은 Immutability
  • 어떠한 변경도 허용하지 않는다는 뜻
  • 주로 가변(Mutable)객체와 구분하는 용도로 사용
  • ex) String 객체는 한번 만들어지면 절대 값을 바꿀 수 없는 불변 객체

불변식 이란?

  • 만드시 만족해야 하는 조건
  • 변경을 허용할 순 있으나, 주어진 조건 내에서만 허용
  • ex) 리스트의 크기는 변할 수 있어도 어떤 때에도 반드시 0 이상이어야 함
  • 가변 객체에도 불변식은 존재할 수 있음
  • 불변은 불변식의 극단적인 예

유독 빌더 패턴이 빛을 발하는 때가 있다.
바로 계층적으로 설계된 클래스일 때다.

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

Pizza를 상속받는 하위 클래스 두 개가 있다.
하나는 일반적인 뉴욕 피자이고, 하나는 칼초네 피자이다.

뉴욕 피자는 크기(size)를 필수 매개변수로 하고,
칼초네 피자는 소스유무 (sauceInside)를 필수 매개변수로 한다.

뉴옥 피자

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 class PizzaTest {
    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();
    }
}

피자 주문할 때 이것저것 설정을 추가하듯이 단계적으로 쌓이는 듯한 느낌이 든다.
NyPizza.Builder와 Calzone.Builder는 Pizza를 상속하고 있지만,반환은 상위 클래스가 정한 것이 아닌 자신의 것으로 반환하고 있다.

이렇게 하게 되면 클라이언트 측에서 굳이 형변환을 하지 않아도 된다는 것이다.

빌더 패턴이 꼭 장점만 있는것은 아니다. 빌더 코드를 따로 짜야하며, 성능에 민감한 상황이라면 노금 문제가 될 수 있다.
그리고 매개변수가 많지 않으면 빌더 패턴을 쓸 이유가 없다.

하지만 api는 시간이 지날수록 비대해진다.
그러니 처음부터 빌드 패턴으로 시작하는게 나을수도 있다.

profile
Search & Backend Engineer

0개의 댓글