💥 개요
정적 팩터리와 생성자에는 똑같은 제약이 하나 있습니다. 매개변수가 많은 경우 문제가 발생하는데, 만약 20개, 30개의 멤버 변수를 가지는 경우가 문제가 발생합니다. 물론 멤버변수가 모두 필수값이라면 클래스로 묶어 처리하는 방법도 있겠지만, 대부분의 경우 대다수의 값이 기본값(0, 혹은 DB에서 정의된 Default Value)이 포함되는 경우가 많이 발생하게 됩니다.
또한 선택적 매개변수를 받아야 하는 경우, 기존에는 원하는 매개변수의 개수에 따라 생성자나 정적 팩터리를 늘려야하는 경우가 발생합니다.
이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은것을 골라 호출하면 됩니다. 하지만 이런 점층적 생성자 패턴은 위에 말한 것처럼 매개 변수의 개수가 늘어날수록 코드가 지저분해지고, 가독성이 떨어지는 문제가 발생합니다. 또한 매개변수가 몇개인지를 세어야하고 타입이 같은 매개변수가 중복해서 있는 경우에는 디버깅을 해도 찾기 힘든 버그가 발생할 여지가 생깁니다.
이는 컴파일 시점에 찾기 힘들고, 런타임 시점에 문제가 발생하거나 심지어 정상적으로 동작하는 크리티컬한 문제로 이어질 가능성이 높습니다.
두번째는 세터를 사용하는 자바빈즈 패턴을 보자면
점층적 생성자 패턴의 단점이 자바빈즈 패턴에서는 더이상 보이지 않습니다. 코드가 길어지긴 했지만, 인스턴스를 만들기 쉽고 더 읽기 쉬운 코드가 되었습니다.
하지만 심각한 단점이 있습니다. 객체 하나를 만드려면 무조건 메서드를 여러개 호출해야하고, 완전히 객체가 생성되지 전에는 일관성이 무너진 상태에 놓이게 됩니다. 또한 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없습니다. 이는 스레드의 안정성을 얻기 힘들고 이런 단점을 완화하려면 추가적으로 freezing하는 추가 개발까지 해야 합니다. 하지만 이런 방법은 다루기 어렵고 다양한 문제가 있습니다.
✅ 해결 방법
바로 점층적 생성자의 안정성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴입니다. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더객체를 얻습니다. 그리고 빌더 객체가 제공하는 체이닝을 통해 선택 매개변수들을 설정하고 마지막으로 매개변수가 없는 build를 호출해 필요한 객체를 얻습니다.
public class BuilderNutritionFacts {
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 {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
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 BuilderNutritionFacts build() {
return new BuilderNutritionFacts(this);
}
}
private BuilderNutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
BuilderNutritionFacts 클래스는 불변이며, 모든 매개변수의 기본값을 한곳에 모아두고 빌더의 세터 메서드들은 자신을 반환하기 때문에 연쇄적으로 호출이 가능합니다. 클라이언트는 사용하기 쉽고 가독성이 좋아보입니다.
또한 빌더의 세터 매서드에서 유효성 검증을 통해 최대한 일찍 발견할 수 있고 build 메서드가 호출되는 생성자에서 여러 매개변수에 걸친 불변식을 검사할 수 있습니다. 잘못된 점을 발견하면 어떤 매개변수가 잘못되었는지를 자세히 알려주는 메시지를 담아 IlegalArgumentException을 던지는 방법으로 개발할 수 있습니다.
또한 빌더 클래스는 계층적으로 설계된 클래스와 함께 쓰기에 좋습니다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하여 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게하는 방법이 있습니다.
각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언합니다. NyPizza.builder는 NyPizza를 반환하고, Calzone.builder는 Calzone을 반환한다는 뜻입니다. 하위 클래스의 메서드가 상의 클래스의 메서드가 정의한 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 타이핑이라고 합니다.
이러한 계층적 빌더를 사용하는 클라이언트의 코드도 앞선 영양정보 빌더를 사용하는 코드와 다르지 않습니다.
생성자로는 누릴 수 없는 이점으로, 가변인수 매개변수를 여러개 사용이 가능합니다. 각각을 적절한 메서드로 나눠 선언하면 됩니다.
아니면 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수를 하나로 모을 수 있습니다. addTopping 메서드가 이렇게 구현된 예시입니다.
빌더 패턴은 상당이 유연하고 빌더 하나로 여러 객체를 순회하며 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만드는것도 가능합니다. 그리고 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수 있습니다.
🩻 단점
빌더 패턴의 단점으로는 빌더 생성 비용이 크지는 않지만 객체를 만드려면 그에 앞서 빌더부터 만들어야 합니다. 즉 타이핑해야 할 코드가 많아지고 개발 속도가 늦춰집니다. 이는 성능이 민감하거나, 속도가 중요한 개발에서는 단점이 될 수 있습니다. 또한 점층적 생성자 패턴이나 일반 생성자 혹은 정적 팩터리보다 코드가 장황해서 매개 변수가 4개 이상이 되어야 값어치를 한다고 합니다.
하지만 실 개발시에는 10개 이상 혹은 20개 이상의 비즈니스 요구사항이 많은 경우에 사용하는것이 개인적으로는 더 적합해 보입니다.
롬복에서 빌더를 지원해주기도 하지만 개발 시 라이브러리 사용을 꺼려하는 백엔드 개발자분들도 계시기 때문에 이런점은 단점이 될 수 있을 것 같습니다.
하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있고 생성자나 정적 팩터리로 시작했다가 리팩터링 할 수 있겠지만 이전에 만들어둔 생성자와 정적 팩터리를 모두 수정하려면 힘들기 때문에, 애초에 빌더로 시작하는 방법이 나은 경우가 많기 때문에 설계시 정적 팩터리를 고려해볼만 한 것 같습니다.
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 낫습니다.
매개변수 중 다수가 필수가 아니고 비슷한 타입이라면 더 그렇습니다.
빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 안전합니다.
Github(Source Code)
https://github.com/devMtn30/Effective_Java_3E/tree/master/src/main/java/chap02/item2
'Study > 이펙티브 자바' 카테고리의 다른 글
[Effective Java 3E] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.08.19 |
---|---|
[Effective Java 3E] 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2023.08.17 |
[Effective Java 3E] private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2023.08.13 |
[Effective Java 3E] 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2023.08.13 |