[Effective Java] 아이템 2: 생성자에 매개변수가 많다면 빌더를 고려하라

Loopy·2022년 5월 13일
0

이펙티브 자바

목록 보기
2/76
post-thumbnail

선택 매개변수가 많을 경우 활용 가능한 여러 패턴들 중에, 빌더 패턴 이전에 어떠한 패턴들을 썼는지 거슬러 올라가보면서 빌더 패턴의 중요성에 대해 알아보자.

☁️ 점층적 생성자 패턴

우리는 정적 팩터리와 생성자를 통해 객체를 생성할 수 있다. 하지만 제약이 하나 존재하는데, 선택적 매개변수가 많을수록 대응이 힘들어진다는 것이다. 이런 경우 보통 점층적 생성자 패턴을 즐겨 사용한다.

점층적 생성자 패턴이란, 필수 매개변수만 받는 생성자, 필수 + 선택 매개변수 N개를 받는 생성자 형태로 선택 매개변수를 전부 다 받는 생성자까지 늘려나가는 방식의 패턴을 말한다.

public class NutritionFacts {
	private final int servingSize;   //필수
    private final int servings;      //필수
    private final int calories;      //선택
    private final int fat;           //선택
    ...
    
    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){
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
  }
}

점층적 생성자 패턴의 한계

점층적 생성자 패턴은, 사용자가 설정하길 원치 않는 매개변수에는 필수적으로 0을 넘겨줘야 하는 문제가 발생한다. 즉, 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어려워지는데 클라이언트가 실수로 매개변수의 순서를 바꿔도 컴파일러는 에러를 잡아내지 못해 런타임에 엉뚱한 동작을 하게 되는 것이다.

항상 말하지만, 제일 중요한 것은 컴파일 타임에 에러를 잡아낼 수 있어야 한다.

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0);

☁️ 자바빈즈 패턴(JavaBeans Pattern)

자바빈즈 패턴이란, 매개변수가 없는 생성자로 객체를 만든 후 세터 메서들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.

public class NutritionFacts {
	private final int servingSize = -1;   //필수
    private final int servings = -1;      //필수
    private final int calories = 0;      //선택
    private final int fat = 0;           //선택
    
    public MutritionFacts(){}
    
    //setter method
    public void setServingSize(int val){servingSize=val;}
    public void setServings(int val){servings=val;}
    public void setCalories(int val){calories=val;}
    public void setFat(int val){fat=val;}
}

인스턴스는 다음과 같은 방식으로 만들면 된다.

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);

자바빈즈 패턴의 한계

  1. 객체 하나를 만들려면 메서드를 여러개 호출해야 한다.
  2. 매개변수가 유효한지를 생성자에서 확인 하지 못하기 때문에, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태가 된다.

즉, 자바빈즈 패턴에서는 이처럼 일관성이 무너지는 문제로 인해 클래스를 불변으로 만들 수 없게 되고, 추가 작업이 없는 한 스레드 안전성 또한 얻지 못한다.

✏️ 일관성(Consistency)이 무너지면?
버그를 심은 코드와 버그 때문에 런타임에 문제를 겪는 코드가 물리적 거리로 인해 디버깅이 힘들어진다.

☁️ 빌더 패턴(Builder Pattern)

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

빌더 객체 만드는 법

  1. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(or 정적 팩터리)를 호출빌더 객체를 얻는다.
  2. 빌더 객체가 제공하는 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
  3. 매개변수가 없는 build() 메서드를 호출해 필요한 불변 객체를 얻는다.
public class NutritionFacts {
	private final int servingSize;   
    private final int servings;      
    private final int calories;     
    private final int fat;         

    private NutritionFacts(Builder builder){
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;  
    }
    
	public static class Builder{
    
      //필수 매개변수
      private final int servingSize;  
      private final int servings;   

      //선택 매개변수(기본 값 초기화)
      private final int calories = 0;      //선택
      private final int fat = 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 NutritionFacts build(){
      	return new NutritionFacts(this);  //instance 생성
      }
   }
}

해당 클래스는 한번 값을 세팅해주면 외부에서 변경할 방법이 없으니 불변이되며, 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 (fluent API/ method chaining) 할 수 있다. 인스턴스를 생성하는 방식은 다음과 같다.

NutritionFacts cocaCola = NutritionFacts.Builder(240,8)
	.calories(100)
    .build();

빌더 패턴은 명명된 선택적 매개변수를 흉내낸 것이다.

🔖 불변(Immutable) 과 불변식(invariant)이란?
1. 불변 : 어떠한 변경도 허용하지 않는다는 뜻으로, 변경을 허용하는 가변 객체와 구분하는 용도로 쓰인다. ex) String
2. 불변식 : 변경에 관계 없이 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 말한다. 예를 들어, 기간을 표현하는 Period 클래스에서 start와 end값이 역전되면 불변식이 깨졌다고 볼 수 있다.

☁️ 빌터 패턴의 장점

1. 빌더 패턴은, 계층적으로 설계된 클래스와 함께 쓸 수 있다.

추상 클래스에는 추상 빌더, 구체 클래스에는 구체 빌더를 갖는것 처럼 각 계층의 클래스에 빌더를 멤버로 정의하면 된다. 예를 들어, 피자의 다양한 종류를 표현하는 계층구조의 루트에 놓인 추상 클래스를 봐보자.

public abstract class Pizza {
	public enum Topping {HAM, MUSHROOM, ONION}
    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();
   		}
   
        abstarct Pizza build();

        //하위 클래스는 이 메서드를 재정의하여 "this"반환
        protected abstract T self();
   }

  Pizza(Builder<?> builder){    //생성자
      toppings = builder.toppings.clone();  //item 50 : 방어적 복사
  }
}

Pizza.Builder 클래스는 재귀적 타입 한정(item 30)을 이용하는 제네릭 타입이며, 추상 메서드인 self를 통해 하위 클래스에서 형변환 하지 않고도 메서드 연쇄가 가능하게 하고 있다.

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
        public protected Builder self(){
        	return this;
        }
    }
    
   private NyPizza(Builder builder){
    	super(builder);    // 토핑 채우기
        size = builder.size;
   }
}

각 하위 클래스 빌더가 정의한 build() 메서드에서 해당하는 구체 하위 클래스가 반환되고 있는 것을 볼 수 있는데, 이렇게 하위 클래스의 메서드가 상위 클래스에서 정의한 반환 타입이 아닌 그 하위 타입을 반환하는 기능을 공변 반환 타이핑(convariant return typing) 이라 한다.

아래의 객체를 생성하는 코드를 봐보자.

NyPizza pizza = new NyPizza.Builder(SMALL)
	.addTopping(SAUSAGE)
    .addTopping(ONION)
    .build();
    
Calzone calzone = new Calzone.Builder()
	.addTopping(HAM)
    .sauceInside()
    .build();

생성자를 사용했을때와 다르게, 가변인수 매개변수(여러개)를 각각의 적절한 메서드로 나눠 선언하면 여러개 사용 할 수 있다는 장점이 있다.

2. 빌더 패턴은 유연하다.

빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.

☁️ 빌더 패턴 단점

객체 생성을 위해, 앞서서 빌더를 만들어야 하는 것이 문제가 될 수 있다. 또한, 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상 되어야 그 값어치가 있다.

하지만, 롬복이라는 기술을 이용하면 어노테이션 하나로 빌더 생성이 편리하게 가능하다.

핵심 정리
생성자는 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하자. 특히, 매개변수 중 다수가 필수가 아니거나 같은 타입인 경우에 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글