본문 바로가기

Reading/Effective Java

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

정적 팩터리 메서드와 생성자는 매개변수가 많이 필요한 경우 적절한 대응이 어려워진다.

이에대한 3가지 대안이 있다.

1. 점층적 생성자 패턴(telescoping constructor pattern)


 

//필수 매개변수만 받는 생성자
public NutritionFactsConstructor(int servingSize, int servings) {
  this(servingSize,servings,0);
}

// 필수 매개변수 + 선택 매개변수 1개
public NutritionFactsConstructor(int servingSize, int servings, int calories) {
  this(servingSize,servings,calories,0);
}

// 필수 매개변수 + 선택 매개변수 2개
public NutritionFactsConstructor(int servingSize, int servings, int calories, int fat) {
  this(servingSize,servings,calories,fat,0);
}

// 필수 매개변수 + 선택 매개변수 3개
public NutritionFactsConstructor(int servingSize, int servings, int calories, int fat, int sodium) {
  this(servingSize,servings,calories,fat,sodium,0);
}

// 필수 매개변수 + 선택 매개변수 4개
public NutritionFactsConstructor(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;
}

 

필수 매개변수만 받는 생성자에서부터 모든 매개변수를 다 받는 생성자까지 늘려가는 방식이다.

매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

중간에 필요없는 매개변수에도 값을 넣어줘야하고(현재는 0), 값의 의미가 헷갈리고 결국 길어지게된다.

 

2. 자바빈즈 패턴(JavaBeans pattern)


 

public NutritionFactsJavaBeans() { }

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; }
public void setSodium(int val)       { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }

 

매개변수가 없는 생성자로 객체를 만든 후, setter 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.

 

하지만 이 방법은

1. 객체 하나당 여러개의 setter 메서드를 호출해야한다.

2. 객체가 생성되기 전까지 일관성이 무너지고 찾기 힘든 런타임 에러가 발생할 수 있어 디버깅이 힘들어진다.

3. 클래스를 불변으로 만들 수 없기때문에 스레드 안정성을 얻을 수 없다.

 

물론 이러한 단점을 완화하고자 수동으로 freezing할 수 있지만 컴파일러가 보증할 방법이 없다.

 

3. 빌더 패턴(Builder pattern)


 

1. 필수 매개변수만으로 생성자(혹은 정적 팩터리 메서드)를 호출해 빌더 객체를 얻는다.

2. 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.

3. 매개변수가 없는 build 메서드를 호출해 객체를 얻는다.

 

소스 참고: https://github.com/kkt219a/TIL/blob/main/effective-java-3rd/src/main/java/effective/item2/NutritionFactsBuilder.java

 

builder의 setter들은 builder 자신을 반환해 연쇄적으로 호출하는데, 이를 플루언트 API(fluent API) 혹은 메서드 연쇄(method chaining)라 한다.

 

그리고 아래 적절한 위서 불변식(invariant)을 검사 후 어떤 매개변수가 잘못되었는지 IllegalArgumentExcption을 던져 알려줄 수 있다.

1. builder의 생성자

2. builder의 setter 메서드

3. build() 메서드가 호출하는 생성자

 

*불변식: 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야하는 조건. 변경을 허용할 수 는 있으나 주어진 조건 내에서 허용. 따라서 가변 객체에도 불변식은 존재할 수 있고, 불변은 불변식의 극단적인 예라고 볼 수도 있다.

 

계층적인 설계에서 사용되는 빌더패턴


 

추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게한다. 

소스 참고: https://github.com/kkt219a/TIL/tree/main/effective-java-3rd/src/main/java/effective/item2/hierarchicalbuilder

 

1. 재귀적 타입 한정(recursive type bound): 자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정하는 것

 

// 빌더 자기 자신의 하위타입을 받는 빌더.
abstract static class Builder<T extends Builder<T>> {}

 

2. 시뮬레이트한 셀프 타입(simulated self-type): self 타입이 없는 자바를 위한 우회 방법

 

// 형 변환을 하지 않고도 메서드 연쇄를 지원할 수 있다.
protected abstract T self();

 

3. 공변 반환 타이핑(convariant return typing): 하위 클래스들의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌 그 하위타입을 반환하는 기능 

 

abstract Pizza build();

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

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

 

그리고 가변인수(varargs) 매개변수를 생성자나 팩토리는 맨 마지막 변수에 하나밖에 못쓰지만,

(가변인수 외에 다른 매개변수가 있으면 가변인수는 마지막에 선언해야 하기때문에)

 

public 생성자(String a, String...b){
	...
}

 

빌더를 이용하면 여러 개 사용할 수 있다.

각각을 적절한 메서드로 나눠 선언하거나, 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수 있다.

Pizza의 addToping이 그 예시다. 

 

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

객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.

 

 

물론 단점도 존재한다.

1. 객체를 만들기 앞서 빌더부터 만들어야한다. 생성 비용이 크진 않지만 성능에 민감하다면 문제가 될 수 있다.

2. 점층적 생성자 패턴보단 코드가 장황해 매개변수가 4개 이상은 되야 값어치를 한다.

 

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.