public class Book {
public static void main(String[] args) {
Info info1 = new Info();
Info info2 = new Info("Shin");
Info info3 = new Info("Shin", 100000000);
Info info4 = new Info("Shin", 100000000, 12344);
}
}
public class Info {
public Info() {}
public Info(String author) {}
public Info(String author, int phoneNum) {}
public Info(String author, int phoneNum, int address) {}
}
익숙하지만 생각해보면 문제가 있다.
“전화번호는 없고 주소만 있으면 어떡하나요?”
Info(String author, int address) 이런 건 불가능하다. 이미 String, int를 인자로 받는 생성자가 존재하기 때문이다.
하지만 편법을 써서 Info(int address, String author)로 만드는 건 가능하다. 하지만 엄청나게 좋지 않은 코드이다.
그럼 이 코드들을 정적 팩토리 메소드(static factory method)를 활용하여 구현해보자.
public class Book {
public static void main(String[] args) {
Info info1 = Info.makeWithAuthor("Shin");
Info info2 = Info.makeWithAuthorAndAddress("Shin", 12344);
Info info3 = Info.makeWithAuthorAndPhone("Shin", 1233323);
}
}
class Info {
public Info() {}
public Info(String author, int phoneNum, int address) {}
public static Info makeWithAuthor(String author) {
return new Info(author, 0, 0);
}
public static Info makeWithAuthorAndPhone(String author, int phoneNum) {
return new Info(author, phoneNum, 0);
}
public static Info makeWithAuthorAndAddress(String author, int address) {
return new Info(author, 0, address);
}
}
이렇게 만들게 되면 넣는 변수의 용처도 파악하기 쉽고 전과 같은 문제는 없어보인다! 그러면 정적 팩토리 메소드의 장점과 단점을 알아보자.
장점은 크게 다섯 가지로 볼 수 있는데, 첫번째는 “이름을 가질 수 있다”
라는 것이다.
이름을 가지게 된다면 반환되는 객체의 특성을 확인하기 쉽다는 장점이 있다. 위에 있는 예시에서 어떤 정보를 기반으로 객체가 생성되는지 쉽게 파악할 수 있다는 것이다.
책에서 제시된 예시인 BigInteger.probablePrime을 살펴보자.
public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
return (bitLength < SMALL_PRIME_THRESHOLD ?
smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}
이 예시에서도 probablePrime이라 해서 소수인 BigInteger를 반환해줄 것이라는 생각이 들 것이다. 이런 점에서 public 생성자보다 정적 팩토리 메소드가 좋은 것이다.
두번째는 “호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다”
이다.
이 점으로 불변 클래스(immutable class)는 인스턴스를 미리 만들어놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체의 생성을 피할 수 있다.
책에서 제시한 예시로 Boolean.valueOf(Boolean)의 코드를 살펴보자.
public final class Boolean implements java.io.Serializable,
Comparable<Boolean>, Constable
{
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
@IntrinsicCandidate
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
}
여기서 보이듯이 Boolean TRUE, FALSE를 미리 final로 생성해놓고 valueOf를 호출할 때 해당 인스턴스를 반환하는 것을 볼 수 있다.
이런 식으로 반복되는 요청에 동일하게 객체를 반환해주는 클래스를 인스턴스 통제(instance-controlled) 클래스라고 한다. 이런 클래스는 a == b, a.equals(b)가 될 수 있도록 통제할 수 있고, 이후에 나오는 플라이웨이트 패턴(동일한 내적 속성 객체를 공유하는 객체를 대량으로 생상할 수 있도록 하는 디자인 패턴)을 만들 때 좋다.
세번째는 “반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다”
이다.
이 특성은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 엄청난 유연성을 제공한다.
public class Animal {
private String name;
private int age;
public static Rabbit rabbit() {
return new Rabbit();
}
}
class Rabbit extends Animal {}
위와 같이 하위 타입의 객체를 언제든 반환할 수 있다.
이와 연결하여 네번째 장점을 알아낼 수 있다. “입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다”
이다.
위의 코드를 약간만 수정할 것이다.
public class Rabbit {
private String name;
private String color;
public static Rabbit filterRabbitColor(String color) {
if (color == "Black") return new BlackRabbit();
else if (color == "Yellow") return new YellowRabbit();
else if (color == "White") return new WhiteRabbit();
else return new GrayRabbit();
}
}
class BlackRabbit extends Rabbit {}
class YellowRabbit extends Rabbit {}
class WhiteRabbit extends Rabbit {}
class GrayRabbit extends Rabbit {}
인자에 따라 다음과 같이 반환하는 객체의 클래스를 조정할 수 있는 것이다.
책에서는 JDK의 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);
}
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {}
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {}
RegularEnumSet과 JumboEnumSet에 대한 세세한 설명은 필요할 것 같지 않고 메소드의 반환 값을 알면 구조를 알 필요가 없다!
마지막 장점으로 “정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다”
이다.
책에서는 다음과 같이 적혀있다.
📖 이런 유연함은 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다. 대표적인 서비스 제공자 프레임워크로는 JDBC(Java Database Connectivity)가 있다. 서비스 제공자 프레임워크에서의 제공자(provider)는 서비스의 구현체이다. 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여…..설명만 봐서는 무슨 말을 하려는지 모르겠다..
그래서 책에 있는 예제인 JDBC를 열어봤다.
우선 설명을 보면 크게 3가지 핵심으로 구성되어 있다고 한다.
여기서 Service Access API가 정적 팩토리 메소드이다. JDBC에서는 Connection이 Service Interface 역할을, DriverManager.registerDriver가 Provider Registration API를 DriverManager.getConnection이 Service Access API이다.
public interface Connection extends Wrapper, AutoCloseable {}
(출처 : https://dyjung.tistory.com/50)
여기서 보면 각 DB의 종류에 따라 Connection을 다른 방식으로 구현해 두고 이것을 Driver라고 한다.
책에서 제시된 단점은 크게 두가지이다.
첫번째는 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없다.
이다.
두번째는 명세서에 생성자처럼 명확하게 드러나지 않으니 프로그래머는 사용하기 어렵다
라는 것이다.
→ 이를 타개하기 위해서 API 문서를 잘 써놓고 메소드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화시켜야 할 것이다. 다음은 그 방식들이다.
핵심 정리
정적 팩토리 메소드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩토리 메소드를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하는 습관이 있으면 고치자.