정적 팩토리와 생성자에는 동일한 제약 조건이 있다. -> 선택적 매개변수가 많을 때 적절히 대응하기 어렵다.
매개 변수가 많아질 경우 사용할 수 있는 세 가지를 고려해 볼 수 있다.
텔레스코핑 생성자 패턴
자바빈즈 패턴
빌더 패턴
(1) Telescoping constructor pattern
필수 매개변수와 선택 매개변수를 갖는 생성자의 형태를 띤다. 아래에 예시를 나타내겠다.
필수 매개변수만 갖는 생성자
필수 매개변수 + 하나의 선택 매개변수 생성자
필수 매개변수 + 두 개의 선택 매개변수 생성자
...
...
위와 같이 필수 매개변수만 갖는 생성자를 생성할 수 있고, n개의 선택 매개변수를 생성하는 생성자를 함께 갖는 경우의 방식이다.
예제) 식품의 영양 정보를 표현하는 클래스를 생각해보자, 식품의 양, 개수, 칼로리, 총 지방, 포화 지방 등 20개 이상의 선택 필드를 가질 수 있다. 몇 가지만 값을 가지고 대부분은 0의 값을 가진다. 생성자를 어떻게 만들까?
public class NutritionFacts{
private final int servingSize; //required
private final int servings; //required
private final int calories; //optional
private final int fat; //optional
private final int sodium; //optional
private final int carbohydrate; //optional
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);
}
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;
}
}
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
위와 같이 오버로딩을 사용하여 나타내는 방식이 점층적 생성자 패턴이라고 한다.
대신 위와 같은 방법을 사용하면 원하지 않은 변수에도 초기값을 설정해야 한다.
오버 로딩 -> 같은 이름이지만 변수의 개수 또는 타입이 달라 여러 개를 정의할 수 있다.
단점
위처럼 점층적 생성자 패턴도 쓸 수 있지만, 매개변수의 개수가 많아지면 클라이언트 코드 작성이 힘들고 가독성이 떨어진다.
저 생성자 코드를 보면 내가 무엇에게 값을 주는지 알기 어렵고 어떤 파라미터에 값을 입력하는지 주의해서 봐야 한다.
또한, 동일한 타입의 매개변수가 늘어져 있다면 찾기 어려운 버그로 이어질 수 있다.
클라이언트가 실수해서 다른 생성자를 선택하거나, 알아채지 못할 수 있다.
(2) JavaBeans pattern
매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 이용하여 원하는 값을 설정하는 방식
public class NutritionFacts{
private int servingSize = -1; //required
private int servings = -1; //required
private int calories = 0; //optional
private int fat = 0; //optional
private int sodium = 0; //optional
private int carbohydrate = 0; //optional
public NutritionFacts(){}
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 };
}
위처럼 아까 Telescoping constructor pattern 보다는 생성자 코드가 길지만 보기는 쉽게 변한다는 장점이 있다.
어떻게 매개변수에 setter 메서드가 존재하기에
cocaCola.setServingSize(240); 과 같이 값을 지정해주면 된다.
단점
1. 여러 메소드 호출로 나누어져 인스턴스가 생성되므로, 생성 과정이 이뤄지는 동안 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓기에 된다.
2. 클래스를 불변으로 만들 수 없어서 프로그래머가 추가 작업을 해주어야 한다.
(3) Builder pattern
-> 점층적 생성자 패턴의 안전성 + 자바 빈즈의 가독성
장점
1. 작성이 쉽고 가독성이 좋다. -> 롬복을 이용하면 더 쉽게 된다.
2. 불변 규칙을 이용할 수 있고, 검사 또한 가능하다. -> IllegalStateException을 통해 예외 처리 가능하다.
단점
1. 어떤 객체를 생성하기 위해 빌더를 만들어야 가능하기에 성능상 문제가 될 수 있다.
2. 매개 변수가 4개 이상이 될 경우 사용하는 것이 좋다. Telescoping 보다 더 긴 코드가 생성될 수 있다.
-> 그래도 Builder pattern을 염두에 두고 코드를 구현해라.
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 = serginsSize;
this.servings = servings;
}
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 NutirionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.fat;
carbohydrate = builder.carbohydrate;
}
}
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
정리
생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바 빈즈보다 훨씬 안전하다.
public class item1 {
private String name;
// public 생성자
public item1(String name) {
this.name = name;
}
// static factory method
public static item1 myName(String name) {
return new item1(name);
}
public static void main(String[] args) {
item1 my = myName("haessae0"); // static factory method
item1 my2 = new item1("haessae0"); // public 생성자
}
}
이렇게 클래스는 정적 팩토리 소드를 제공할 수 있다.
장점
– 이름을 가질 수 있다.
– 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
– 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
– 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
– 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
단점
– 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가
– 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.
장점
1. 이름을 가질 수 있다.
정적 팩토리 메소드는 이름만 잘 지으면 반환되는 객체의 특성을 명시적으로 표현할 수 있다.
public class item1 {
private String name;
// public 생성자
public item1(String name) {
this.name = name;
}
// static factory method
public static item1 myName(String name) {
return new item1(name);
}
public static void main(String[] args) {
item1 my = myName("haessae0"); // static factory method
item1 my2 = new item1("haessae0"); // public 생성자
}
}
앞서 작성한 코드를 보자면
1.
public 생성자로 만든 것 -> 호출하였을 때, ‘haessae0’라는 것이 어떤 인스턴스 변수인지 알기 어렵다. -> 클래스 이름인 ‘item1’에게 전달하여 생성하는 것이기 때문이다.
2.
정적 팩토리 메소드로 만든 것 -> 호출하였을 때, ‘haessae0’라는 것이 myName이라는 것을 알 수 있다. -> 내 이름이 haessae0라는 것을 알 수 있다.
당연히 알기 쉬운 정적 팩토리 메서드를 사용할 것이다.
또한,
public 생성자는 하나의 시그니처로 하나만 생성할 수 있다.
public class item1 {
private String name;
private String birth;
// public 생성자
public item1(String name) {
this.name = name;
}
// public 생성자 -> 불가
public item1(String birth) {
this.birth = birth;
}
}
이미 name 변수를 받는 생성자가 존재 하기 때문에 birth를 받는 생성자는 생성할 경우 오류를 범하고 만다.
왜? 이미 하나의 변수를 받는 생성자가 존재하기 때문에 사용을 하여도 엉뚱한 메소드를 호출할 수 있다.
하지만! 정적 팩토리 메서드는 가능하게 해준다.
public class item1 {
private String name;
private String birth;
// static factory method
public static item1 myName(String name) {
item1 my = new item1();
my.name = name;
return my;
}
// static factory method
public static item1 myBirth(String birth) {
item1 my = new item1();
my.birth = birth;
return my;
}
}
이렇듯이 하나의 시그니처로 여러 가지를 구현할 수 있다.
2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
불변 클래스는 인스턴스를 미리 만들거나 캐싱하여 재활용하는 방식이기에 불필요한 객체 생성을 막을 수 있다.
대표적으로 Boolean.valueOf(boolean) 메서드가 있다.
public final class Boolean implements java.io.Serializable,Comparable<Boolean> {
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
}
상수 객체로 선언해주기 때문에 새로운 객체를 매번 만들지 않는다.
-> 생성 비용이 큰 객체가 자주 불려지는 상황에 정적 팩토리 메서드를 사용하면 그 성능을 많이 이끌어 낼 수 있다.
또한, 반복되는 객체 요청을 언제 어느 순간에 인스턴스를 살고 죽게 할지 통제하는 인스턴스 통제 컨트롤이 있다.
왜? 사용할까?
– 인스턴스를 통제하면 싱글톤이나 인스턴스화 불가 상태로 만들 수 있다.
– 불변 값 클래스에서 동치인 인스턴스가 하나인 것을 보장 -> a == b일 때만, a.equals(b)가 성립된다.
– 플라이 웨이트 패턴의 근간이고, 열거 타입은 인스턴스가 하나만 만들어지는 것을 보장한다.
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
코드의 유연성을 제공해준다. -> 인터페이스를 정적 팩토리 메서드 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 하다.
동반 클래스에서 주로 사용된다.
자바 1.8 이전 -> 인터페이스에 정적 메서드를 선언할 수 없었다. 따라서 인터페이스에 기능을 추가하기 위해서는 동반 클래스라는 것을 만들어 그 안에 정적 메서드를 추가했다.
-> 굳이 별도의 문서를 찾아가며 수현 클래스가 무엇인지 알아보지 않아도 된다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다른 클래스가 객체를 반환해도 된다.
예시로 EnumSet 클래스가 존재한다.
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
클래스에는 noneOf라는 메서드가 존재하는데 잘 보면 universe의 개수가 64개 이하면 RegularEnumset의 인스턴스를 반환하고, 65개 이상이면 JumboEnumset의 인스턴스를 반환한다.
두 타입 모두 알려지지 않기 때문에 클라이언트는 인지하지 않아도 되며 나중에 삭제하거나 새로운 타입을 만들어도 문제없이 사용할 수 있다. 그저 Enumset의 하위 클래스에만 존재하면 된다.
5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
이러한 유연함은 서비스 제공자 프레임워크를 만드는 기반이 된다. -> 대표 JDBC
서비스 제공자 프레임워크의 구성요소
Service Interface : 구현체의 동작 정의
Connection
Provider Registration API : 제공자가 구현체를 등록할 때 사용
DriverManager.registerDriver
Service Access API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
DriverManager.getConnection
Service Provider Interface : 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체
Driver
단점
– 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가
– 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.
1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스 생성 불가
java.util.Collections로 만든 구현체는 상속할 수 없다.
2. 정적 팩토리 메서드는 프로그래머가 찾기 어렵다.
Javadoc 문서에서 따로 정리하지 않는다. API 문서를 잘 써놓고 메소드 이름도 알려진 규약을 따라 짓는 식으로 문제를 해결해줘야 한다.