book/Effective java

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

it's woo 2021. 8. 31. 23:32
 

이펙티브 자바 3/E - 교보문고

프로그래밍인사이트 | 자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브러리의 최신 기능을 십분 활용

www.kyobobook.co.kr

개요


 

정적 팩터리와 생성자에는 선택적 매개변수가 많을때 적절히 대응하기 어렵다.
책에나온 3가지방법 점층적 생성자 패턴 , 자바빈즈 패턴 , 빌더 패턴 을 사용해보자

점층적 생성자 패턴

  • 필수 매개변수만 받는 생성자,그리고 선택 매개변수를 1개씩 추가하여 받는 생성자들을 만든다.
  • 원치 않는 매개변수까지 포함할때가 있는데 어쩔 수 없이 매개변수에 값을 지정해줘야 한다.
  • 매개변수 개수가 많아지면 코드가 작성하기 어려워진다. (매개변수가 무엇인지 헷갈리게 된다.)
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);
    }
    //필수 매개변수와 선택 매개변수를 받는 생성자들
    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);
    }
    //필수매개변수와 soduim 값만 설정할 수 없다. 어쩔수 없이 calories fat값도 지정해줘야 한다.
    public NutritionFacts(int servingSize, int servings,
                          int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }
    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 void main(String[] args) {
        NutritionFacts cocaCola =
                new NutritionFacts(240, 8, 100, 0, 35, 27);
    }
    
}

 

자바빈즈 패턴

  • 매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.
  • 객체 하나를 만들려면 여러게의 메서드를 호출해야 한다.
  • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상테에 놓이게 된다.
  • 인스턴스를 만들기 쉽고 가독성이 높다.
public class NutritionFacts {
    // 매개변수들은 (기본값이 있다면) 기본값으로 초기화된다.
    private int servingSize  = -1; // 필수; 기본값 없음
    private int servings     = -1; // 필수; 기본값 없음
    private int calories     = 0;
    private int fat          = 0;
    private int sodium       = 0;
    private int carbohydrate = 0;

    public NutritionFacts() { }
    // Setters
    //점층적 생성자 패턴보다 짧아 가독성이 좋다
    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; }

    public static void main(String[] args) {
        //객체를 생성하고 세터를 통해 값을 설정한다.
        NutritionFacts cocaCola = new NutritionFacts();
        //객체 하나를 완성하기 위하여 5개의 메서드를 호출하였다.
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}

 

빌더 패턴

  • 필수 매개변수만으로 생성자(정적팩터리)를 호출해 빌더 객체를 얻는다.
  • 그런 다음 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
  • 마지막으로 매개변수가 없는 build 메서드를 호출해 필요한 객체를 얻는다.
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) {
        //빌더 메소드를 통해 연쇄적으로 메소드를 호출한다음 build()를 통해 객체의 값을 설정한다.
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

공변 반환 타이핑은 뭘까?

하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 말한다. 밑의 예시를 보자

class pizza{
	pizza info() {
		return new pizza();
	}
}


class calzone extends pizza{
	@Override
	calzone info() {
		return new calzone();
	}
	
}

칼조네 피자는 info를 오버라이딩할때 반환형을 칼조네로 한다.
상속관계에 있어 하위타입으로 반환해도 문제가 없다. 이런 특징을 활용하여 형변환에 신경쓰지 않을 수 있다.

 

빌더 패턴의 단점

빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.

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