[Effective Java] item1 - 생성자 대신 정적 팩토리 메소드를 고려하라

신민철·2023년 4월 6일
2

Effective Java

목록 보기
1/23
post-thumbnail
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 Interface : 구현체의 동작을 정의
  • Provider Registration API : provider가 구현체를 등록할 때 사용
  • Service Access API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용

여기서 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 문서를 잘 써놓고 메소드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화시켜야 할 것이다. 다음은 그 방식들이다.

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드
    • Date d = Date.from(instant);
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드
    • Set faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf : from과 of의 더 자세한 버전
    • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance 혹은 getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
    • StackWalker luke = StackWalker.getInstance(options);
  • create 혹은 newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
    • Object newArray = Array.newInstance(classObject, arrayLen);
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메소드를 정의할 때 쓴다. “Type”은 팩토리 메소드가 반환할 객체의 타입이다.
    • FileStore fs = Files.getFileStore(path)
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. “Type”은 팩토리 메소드가 반환할 객체의 타입.
    • BufferedReader br = Files.newBufferedReader(path);
  • type : getType과 newType의 간결한 버전
    • List litany = Collections.list(legacyLitany);

핵심 정리
정적 팩토리 메소드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩토리 메소드를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하는 습관이 있으면 고치자.

0개의 댓글