on
[이펙티브자바 - 아이템2] 생성자에 매개변수가 많다면 빌더를 고려하라
[이펙티브자바 - 아이템2] 생성자에 매개변수가 많다면 빌더를 고려하라
2장. 객체 생성과 파괴
GOAL
1. 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하기
2. 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법
3. 제때 파괴됨을 보장하고, 파괴 전에 수행해야 할 정리 작업을 관리하기
아이템2. 생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩터리와 생성자에는 똑같은 제약이 있다. 바로, 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이다.
이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 어쩔 수없이 그런 매개변수에도 값을 지정해줘야한다.
매개변수 개수가 많아지면, 클라이언트 코드를 작성하거나 읽기 어려워진다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 25, 27); //230, 8,etc.. 이것들이 무엇을 뜻하는지 알 수 없다.
해결책. 자바빈즈 패턴
매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식
public NutritionFacts() {} public void setServingSize(int val) { servingSize = val; } pulic void set Servings(int val) { servings = val; }
덤층적 생성자 패턴의 단점들이 자바빈즈 패턴에는 보이지 않는다. 읽기쉬운 코드가 되었다.
단점
객체 하나를 만들려면 메서드를 여러 개 호출해야 한다. 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. => 클래스를 불변으로 만들 수 없다.
해결책. 빌더 패턴 (Builder pattern)
점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 겸비한 패턴
클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(또는 정적 팩토리)를 호출해 빌더 객체를 얻는다.
public Builder(int servingSize, int servings){ this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { caloreis = val; return this; } private NutritionFacts(Builder builder){ servingSize = builder.servingSize; servings = builder.servings; }
아래는 클라이언트 코드이다. 쓰기 쉽고, 읽기도 쉽다.
빌더의 생성자와 메서드에서 입력 매개변수를 유효성 검사도 할 수 있다.
추가로, 공격에 대비해 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야한다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .claories(100).sodium(35).carbohydrate(27).build();
참고) 불변이란?
어떠한 변경도 허용하지 않는다는 뜼으로, 주로 변경을 허용하는 가변(mutable)객체와 구분하는 용도로 쓰인다.
String객체는 한번 만들어지면 절대 값을 바꿀 수 없는 불변 객체이다.
빌더 패턴
빌터 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 한다.
public abstract class Pizza{ public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } final Set toppings; // 제너릭 타입 // 추상 클래스는 추상 Builder를 가진다. 서브 클래스에서 이를 구체 Builder로 구현한다. abstract static class Builder> { EnumSet 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) 메개변수를 필수로 받는다.
public class NyPizza extends Pizza { public enum Size { SMALL, MEDIUM, LARGE } private final Size size; public static class Builder extends Pizza.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; } }
Pizza의 하위 클래스인 칼초네 피자.
소스를 안에 넣을지 선택하는 매개변수를 필수로 받는다.
public class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder { private boolean sauceInside = false; public Builder sauceInside() { sauceInside = true; return this; } // 상위 클래스의 메서드가 정의한 반환타입이 아닌, 그 하위 타입을 반환하는 기능 // 공변 반환 타이핑 (covariant return typing) @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; } }
각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언하다.
NyPizza.Builer는 NyPizza반환.
Calzone.Builder는 Calzone반환
NyPizza nyPizza = new NyPizza.Builder(NyPizza.Size.MENIDUM) .addTopping(Pizza.Topping.HAM) .addTopping(Pizza.Topping.ONION) .build(); Calzon calzon = new Calzon.Builder() .addTopping(Pizza.Topping.ONION) .sauceINde() .build()
공변 반환 타이핑 (covariant return typing)
상위 클래스의 메서드가 정의한 반환타입이 아닌, 그 하위 타입을 반환하는 기능
이것때문에 클라이언트가 형변환에 신경쓰지 않고도 빌더를 사용할 수 있다.
@Override public Calzone build() { return new Calzone(this); }
장점
빌더 하나로 여러 객체를 순회하면서 만들 수 있다. 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수 있다. 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.
단점
객체를 만들려면, 그에 앞서 빌더부터 만들어야 하다. -> 성능에 민감한 상황에서는 문제가 된다. 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다.
Lombok
lombok으로 @Builder 애노테이션을 붙이면 Builder 패턴을 생성해준다고 한다.
@Builder public class NutritionFacts { @Builer.Default private final int servingSize = 10; private final int servings; private final int calories; public static void main(String[] args) { NutritionFacts.builder() .servingSize() .servings(1) .calories(2) .build(); }
결론
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.
참고
자료는 이펙티브 자바 책과 백기선님의 강의를 들으며 작성했다.
이펙티브자바 - 아이템2 생성자에 매개변수가 많다면 빌더를 고려하라
from http://yeonni-history.tistory.com/22 by ccl(A) rewrite - 2021-09-18 20:27:16