Consider static factory method rather than constructor

jiho·2021년 5월 17일
0

EffectiveJava

목록 보기
2/12

Effective Java 아이템 1의 내용을 정리한 글입니다.

생성자 대신 정적 팩터리 메서드를 고려하라.

보통 자바에서 객체를 생성할 때, new keyword를 통해서 생성합니다.

하지만 모든 프로그래머가 알아둬야할 기법이 있습니다. 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있습니다.

대표적인 예로는 java.lang.Boolean 이 있습니다.

public static Boolean valueOf(boolean b) {
	return b ? Boolean.True: Boolean False;
}

public 생성자 대신 정적 팩토리 메서드를 사용하면 5가지의 장점이 있습니다.

1. public 생성자만으로는 부족한 표현력을 해결

여러 생성자를 오버로딩으로 지원할 때, 특정 생성자가 어떤 의도로 사용되는지 이름만으로 파악하기 어렵습니다. 즉, 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못합니다.


BigInteger randomPrimeNumber = new BigInteger(int, int, random);

BigInteger randomPrimeNumber = BigInteger.probablePrime();

위 두 가지 예를 봤을 때, 이름을 가진 정적 팩토리 메서드가 더욱 직관적입니다.

추가로 생성자의 매개변수의 종류와 수는 같을 경우, 생성자 오버로딩만으로는 한계가 있습니다.

아래와 같이 순서를 바꾸는 방법도 있지만 사용자 입장에서는 사용하기 혼란스러운 API가 될 것 같습니다.

public class Range {
	public Test(A a, B b) { ... }
	public Test(B b, A a) { ... }
}

각 생성자들이 이름을 가질 수 있다면 이런 제약이 사라질 것입니다. 이런 사용이 눈에 보인다면 정적 팩토리 메서드로 바꾸고 각각의 차이를 드러내는 이름을 지어주는 것을 추천합니다.

2. 매번 불필요한 새로운 인스턴스를 생성

불필요한 인스턴스 생성을 하고 싶지 않지만 new 생성자만 지원하게 될 경우, 불필요하게 메모리를 낭비하게 됩니다. 사용하는 클라이언트 코드에서 인스턴스가 있는지 체크하고 없으면 생성하는 식으로 사용하면 되지 않느냐?라고 말할 수도 있겠지만 API를 제공하는 입장에서 그런 부분은 감싸는게 사용자 입장에서 더 API를 단순하게 바라볼 수 있다는 면에서 마땅한 해결책은 아닙니다.

추가로 캐시를 직접 구현해서 사용했을 때 어떤 문제가 생길 지는 API를 개발한 개발자가 아니면 모른체 지나갈 확률이 높습니다.

대표적인 예는 앞서 설명한 java.lang.Boolean 의 valueOf(boolean) 메서드입니다.

위 메서드는 객체를 아예 생성하지 않습니다. 따라서 (특히 생성 비용이 큰) 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 줍니다. GoF 디자인 패턴의 Flyweight 패턴도 이와 비슷한 기법이라 할 수 있습니다.

3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

이번 장점은 반환할 객체의 클래스를 자유롭게 선택할 수 있게하는 "엄청난 유연성"을 제공합니다.

API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있습니다.

이는 인터페이스를 정적 팩터리 메서드의 반환타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 합니다.

대표적인 예로는 java.util.Collections 추상 클래스가 있습니다.
공식문서를 살펴보면 구현체를 모르지만 interface만으로 다양한 구현체(immutable or synchronize 기능을 가진 45개의 유틸리티 구현체)를 생성할 수 있는 정적 팩토리 메서드를 제공합니다. 사용자 입장에서는 해당 구현체가 어떻게 구현됐는지 몰라도 상관없습니다.

이렇게 적용하니 API의 외견이 작아졌고 프로그래머가 익혀야할 개념의 수와 난이도를 낮춰줬습니다. 즉, 개발자는 인터페이스에 노출된 메서드만 사용해도 무방합니다.

그런데 조금 이상한 점은 반환타입의 하위타입 객체를 반환하는 능력이라고했는데 반환되는 Collection 객체들은 Collections가 아닌 Colleciton의 하위 객체입니다.

이유는 java 8 이전에는 interface 에 정적 메소드를 선언할 수 없었에 "Type"인 인터페이스를 반환하는 정적메서드가 필요하면, "Types"라는 이름의 인스턴스화 불가인 동반 클래스(companion class)를 만들어 그 안에 정의하는 것이 관례였다고 합니다.

만약 java 8 이후에 해당 API가 설계 되었다면 다음과 같은 method가 설계되었을 것 입니다.


public interface Collection {
	static <T> List<T> synchronizedList(List<T> list) {
    	return 
    }
	...

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

해당 특성은 한 번에 이해하기 힘들었는데 예시인 EnumSet을 보니 바로 이해가 되었습니다.
EnumSet Class를 한번 살펴보겠습니다.

java.util.EnumSet은 Enum 타입의 요소로 이루어진 Set입니다. EnumSet은 bit vector를 나타내기 위한 클래스입니다.

bit vector 는 매우 메모리, 성능면에서 매우 우수한 자료구조의 형태입니다. 매우 빠르게 조회할 수 있습니다. c, c++ 에서 매우 빠르고 적은 메모리를 사용해서 요소의 포함 여부를 체크하기에 적절한 기법입니다.

EnumSet은 public 생성자 없이 오직 정적 팩터리 메서드만 제공하는데, 생성시 Enum 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환합니다. 원소가 64개 이하면 원소들을 long 변수하나에 관리하는 RegularEnumSet의 인스터스를, 65개 이상이면 long 배열로 관리하는 JumboEnumSet의 인스턴스를 반환합니다. 아마 이렇게 다른 타입을 반환하는 이유는 bit vector를 조금만 알아봐도 알 수 있습니다.

추가로 요소의 개수에 따라 성능을 더 개선할 여지가 있을 경우, 내부적으로 다른 클래스를 추가하기만 하면 될 것 입니다. (유연성)

5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

이런 유연함은 서비스 제공자 프레임워크(Service Provider Framework)를 만드는 근간이 됩니다.

서비스 제공자 프레임워크는 서비스의 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트로부터 구현체를 분리해줍니다.

서비스 제공자 프레임워크란?

대표적인 서비스 제공자 프레임워크로는 JDBC(Java DataBase Connectivity)가 있습니다. JDBC는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API입니다.

자바에서 여러 다른 벤더의 데이터 베이스를 활용해서 쉽게 개발할 수 있는 이유는 각각의 데이터베이스의 클라이언트 API를 JDBC 맞춰서 제공해주기 때문일 것입니다.

이런 설계는 서비스 제공자 패턴을 구현한 JDBC API가 특정 Database 구현에 의존하지않기 때문에 가능합니다.

구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

책 내용만 봐서는 정확히 원리를 이해할 수 없어서 검색을 해보니 스택오버플로우를 보니 이해가 됐습니다.

StackOverFlow Link

위 내용을 정리하자면, JDBC API를 사용하는 클라이언트 코드에서는 특정 서비스의 구체적인 구현에 대해 알 필요가 없습니다. "알 필요 없다"는 말은 우리가 사용할 JDBC를 구현한 데이터베이스 클라이언트 라이브러리가 H2 or MySQL or PostGreSQL인지 코드 수준에서 알 필요없다는 의미입니다.

정적 팩토리 메서드의 단점

앞 내용만 보면 무조건 정적 팩토리 메서드를 사용해야할 것 같지만 단점도 존재합니다.

1. 상속하려면 public 이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 생성할 수 없다.

이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수 있습니다.

2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

엄연히 보면 해당 정적 메서드가 문서를 보기전에는 팩토리 메서드인지 알 수 없습니다.

현재까지는 네이밍 컨벤션에 따라 정적 팩토리 메서드의 이름을 짓는 방식으로 단점을 완화하고 있습니다.

몇 가지 명명 방식을 나열하면

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

정리

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

그리고 정적 팩토리 메서드의 네이밍 컨벤션을 잘 알고 있으면 더욱 빠르게 예상하고 문서를 통해 더 훌륭한 결정을 할 수 있을 것 같습니다.

또 외부 프레임워크가 유연한 설계를 잘 고려했는지 이러한 패턴을 적용했는지 체크해본다면 기술 선정에도 도움이 될 것 같습니다.

Reference

https://docs.oracle.com/javase/tutorial/ext/basics/spi.html

profile
Scratch, Under the hood, Initial version analysis

0개의 댓글