규칙 1에서는 생성자 대신 정적 팩토리 메서드를 사용을 고려해보자는 규칙이었다.
하지만 생성자 뿐만아니라 정적 팩토리 메서드의 인자가 많을 때는 어떤 위치에 어떤 값이 들어가야 하는지 클라이언트가 하나씩 확인하면서 값을 채워줘야 하기 때문에 사용하기 불편한다.
따라서 이를 개선하고자 점진적 생성자 패턴(telescoping constructor pattern)이 만들어졌다.
점진적 생성자 패턴
점진적 생성자 패턴이란 필수 인자를 받는 생성자를 하나 정의하고, 선택적 인자를 받는 생성자를 여러개 점진적으로 만드는 방식이다.
아래는 Netty의 io.netty.handler.proxy.HttpProxyHandler 예시이다.
public final class HttpProxyHandler extends ProxyHandler {
private final HttpClientCodecWrapper codecWrapper = new HttpClientCodecWrapper();
private final String username;
private final String password;
private final CharSequence authorization;
private final HttpHeaders outboundHeaders;
private final boolean ignoreDefaultPortsInConnectHostHeader;
private HttpResponseStatus status;
private HttpHeaders inboundHeaders;
public HttpProxyHandler(SocketAddress proxyAddress) {
this(proxyAddress, null);
}
public HttpProxyHandler(SocketAddress proxyAddress, HttpHeaders headers) {
this(proxyAddress, headers, false);
}
public HttpProxyHandler(SocketAddress proxyAddress,
HttpHeaders headers,
boolean ignoreDefaultPortsInConnectHostHeader) {
...
}
public HttpProxyHandler(SocketAddress proxyAddress, String username, String password) {
this(proxyAddress, username, password, null);
}
public HttpProxyHandler(SocketAddress proxyAddress, String username, String password,
HttpHeaders headers) {
this(proxyAddress, username, password, headers, false);
}
public HttpProxyHandler(SocketAddress proxyAddress,
String username,
String password,
HttpHeaders headers,
boolean ignoreDefaultPortsInConnectHostHeader) {
...
}
아래와 같이 생성자가 필요한 인자에 따라서 여러개가 존재하기 때문에 많은 인자가 들어있는 하나의 생성자보다는 사용하기가 편한다.
하지만 만약 해당 클래스의 필드가 추가되게 되면, 생성자를 추가로 생성해야 한다는 문제가 있다.
옵셔널 한 필드라면 생성자가 하나만 추가하게 코드를 작성할 수 있지만, 필수로 입력해야 하는 필드라면 여태 만들어 둔 생성자에 모두 추가해야 하므로 고쳐야 하는 부분이 더욱 많아진다.
필드 개수에 따라서 생성자를 추가하는 방법이 아닌 다른 방법은 없을까? 다음 대안인 자바빈 패턴(JavaBeans) 패턴을 보자.
자바 빈 패턴
자바 빈 패턴은 한국에서 제일 많이 사용하는 패턴이 아닐까 싶다.
자바 빈 패턴은 디폴트 생성자로 객체를 만들고 필드의 setter 메서드로 필요한 값을 하나씩 호출하여 채우는 방식이다.
public class User {
private String name;
private int age;
private String addr;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setAddr(String addr) {
this.addr = addr;
}
}
public static void main(String[] args) {
User user = new User();
user.setName("홍길동");
user.setAge(10);
user.setAddr("서울");
}
점층적 생성자 패턴보다 복잡하지 않기 때문에 생성하기도 쉬우며, 코드 읽기에도 어려움이 없다.
하지만 자바 빈 패턴은 요즘(?) 대세인 클래스 생성 규칙을 위반한다.
다른 객체 생성 방식과는 다르게 한 번의 함수 호출로 객체 생성을 끝낼 수 없기 때문에 객체의 일관성이 깨질 수 있다.
하나의 함수로 객체를 생성하게 된다면, 해당 객체의 필드에 값들이 유효한지를 보장하도록 코드를 작성할 수 있지만 각각 필드에 대한 setter에서는 그 유효성을 보장하도록 코드를 작성하기 어렵고, 그렇게 한다고 하더라도 잘못 코드를 작성할 경우, 버그를 디버깅하기 어렵다.
또한 setter가 존재한다는 것은 값을 변경할 수 있기 때문에 Immutable 클래스를 만들 수가 없다.
인자가 많은 생성자, 점진적 생성자 패턴, 그리고 자바 빈 패턴을 단점을 커버할 객체 생성 방법은 없을까? 그 대안이 바로 빌더 패턴(Builder Pattern)이다.
빌더 패턴
빌더 패턴은 GOF 디자인 패턴 중에 하나이다. 하지만 여기서 설명하는 빌더 패턴은 GOF에서 설명하는 빌더 패턴을 객체 생성 관점에서 변형에서 사용한 예라고 할 수 있다.
빌더 패턴은 객체를 생성할 클래스안에 빌더 클래스를 작성하고, 이 빌더 클래스를 이용하여 객체를 생성한다.
일반적인 빌더 패턴 만드는 순서는 다음과 같다.
1. 필수로 입력받아야하는 필드만을 가진 생성자를 작성한다.
2. 옵셔널로 입력받아야하는 필드는 setter로 만들고 추가적으로 자신을 리턴하도록 한다.
3. build() 메서드를 만들고 실제 객체 생성자에 자신을 넘겨 해당 객체를 리턴한다.
NutritionFacts cocaCola = new NutriFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(30).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;
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.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);
}
}
}
빌더 패턴은 위에 다른 패턴의 단점을 해결하면서 여러 장점이 존재한다.
1. 빌더 패턴은 build() 메서드에서 한 번에 객체를 생성하기 때문에 필드의 유효성을 판단할 수 있다.
2. build()로 만들어진 객체 자체에 setter을 두지 않도록 하여 Immutable 하게 객체를 생성할 수 있다.
3. 빌더 객체는 재사용할 수 있다.
NutriFacts.Builder builder = new NutriFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(30);
NutriFacts n1 = builder.build();
builder.sodium(0);
NutriFacts n2 = builder.build();
4. 빌더에 부가적인 기능을 추가 할 수 있다.
public class NutritionFacts {
...
public static class Builder {
private static int count = 0;
...
public NutritionFacts build() {
count++;
return new NutritionFacts(this);
}
}
}
단점으로는 코드의 양이 증가할 수 있으며, 다른 패턴보다는 성능이 약간 떨어진다.
하지만 요즘 여러 라이브러리에서 어노테이션만으로 빌더 패턴을 적용할 수 있도록 제공하고 있기 때문에 첫 번째 단점은 쉽게 해결할 수 있다. (lombok 등)
@Builder
public class Member {...}
'Java > [책] 이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] 규칙6. 메모리 누수 (leak) (0) | 2021.11.14 |
---|---|
[이펙티브 자바] 규칙5. 불필요하게 객체를 여러번 만들지 않기 (String, Calendar 등) (0) | 2021.11.06 |
[이펙티브 자바] 규칙4. Util 클래스와 같이 객체 생성이 필요없는 클래스는 private 생성자를 사용 (0) | 2021.10.24 |
[이펙티브 자바] 규칙3. 싱글턴 패턴 (0) | 2021.10.20 |
[이펙티브 자바] 규칙1. 생성자 대신 정적 팩터리 메서드 (1) | 2021.10.04 |