생성자 대신 정적 팩토리 메서드를 고려하라
핵심 요약
- 객체 생성에는 보통 생성자만 있다고 생각하기 쉽다.
- 정적 팩토리 메서드를 객체 생성 용도로 쓰는 것도 경우에 따라 좋다.
생성자 대신 정적 팩터리 메서드를 만들면 가지는 장점들
장점1: 생성자가 이름을 가질 수 있다.
public class Test {
@org.junit.jupiter.api.Test
public void bigInteger() {
BigInteger bigInteger = new BigInteger(10, 100, new Random());
BigInteger probablePrime = BigInteger.probablePrime(10, new Random());
System.out.println("bigInteger = " + bigInteger);
System.out.println("probablePrime = " + probablePrime);
}
}
- 위 예에서 2가지 방식으로 소수 형태의
BigInteger
를 생성할 수 있다.- 생성자는 단순히 봐서 의도를 알기 어렵다. 아마 랜덤한 숫자를 생성하는 것 같다.
probablePrime
이라는 정적 팩토리 메서드를 통한 객체 생성은 한눈에 봐도 의도가 명확하다.
물론 문서를 찾아봐도 되지만, 그 시간을 줄이고 더욱 명확한 이해와 함께 코드를 작성할 수 있다.
장점2: 매 호출 시 인스턴스를 새로 생성할 필요가 없다.
@jdk.internal.ValueBased
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);
...
}
Boolean
클래스 같은 경우 상태가 한정적이다.- 멤버 필드인
value
에 들어올 수 있는 값은true
혹은false
이다. - 매번 인스턴스를 새로 만들 필요 없이
.valueOf()
메서드를 이용하면,static
한 객체를 반환해준다.
- 멤버 필드인
- 플라이웨이트 패턴 참조링크에서 플라이웨이트 패턴의 예제를 볼 수 있다.
- 용어가 생소할 수는 있어도 그냥
static
한 저장소에 객체를 저장해놓고 불러다 쓰는 것일 뿐이다. - 항상 같은 객체를 반환하는 팩토리 메서드를 정적 팩토리 메서드라 하는데, 이런 방식을
인스턴스 통제(instance-controlled)
라고 한다.- 인스턴스 통제는
싱글톤
,인스턴스화 불가
,1개의 동치 보장 (equals와 ==이 같은 결과 반환)
에 다양하게 사용된다. - 열거(
enum
) 타입은 인스턴스가 하나임이 보장되는 타입이다.
- 인스턴스 통제는
- 용어가 생소할 수는 있어도 그냥
장점3: 반환 타입의 하위 타입을 반환하는 것도 가능하다.
- 이를테면 인터페이스인
List
를 반환하는 메서드를 만들고, 매개변수에 따라ArrayList
,LinkedList
등을 반환하는 것이 가능한 것이다.- 기본 클래스 생성자를 사용하면
인터페이스
반환이 불가능하다.
- 기본 클래스 생성자를 사용하면
- 클라이언트의 입장에서는 오직
인터페이스
만을 가지고 코드를 작성할 수 있게 되어 매우 유연해진다.인터페이스
기반으로 코딩하는 것은 일반적으로 좋은 습관이다.
- 자바 컬렉션 프레임워크는 내부적으로 45개의 유틸리티 구현체를 제공하는데, 이 구현체 대부분을 단 하나의 인스턴스화 불가 클래스인
java.util.Collections
의 정적 팩토리 메서드에서 얻게 했다. - 자바 8부터는 인터페이스에
public
정적 메서드 작성을 지원하기 때문에 인터페이스를 반환하는 여러가지 구현체를 만들어둘 수 있다.
장점4: 인자에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 인터페이스를 반환하면, 하위 타입 어떤 객체를 반환하든 상관 없기 때문이다. (
장점3
과 일맥상통하는 부분이 있다.) List
를 반환하는 정적 메서드가 있다면, 인자에 따라ArrayList
와LinkedList
를 반환하는 것이 가능하다.- 매개변수를 하나 받아서 대량의 데이터 삽입 삭제가 일어난다면
LinkedList
를 받고, 아니라면ArrayList
를 받을 수 있다.
- 매개변수를 하나 받아서 대량의 데이터 삽입 삭제가 일어난다면
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);
}
책에서 나온
EnumSet
의 예제이다.EnumSet
이란, 그냥Set
인데,Enum
타입에 특화된 것이다. 위의EnumSet
은 데이터 64개를 기준으로RegularEnumSet
과JumboEnumSet
중 어느것을 반환할지 결정한다.
장점5: 정적 팩토리 메서드 작성 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
- 이 부분도 사실
장점3
과장점4
의 연장선이다. JDBC
와 같은 서비스 제공자 프레임워크에서 이러한 기능을 적극 활용한다.JDBC
는 대표적인 서비스 제공자 프레임워크이다.- 서비스 제공자 프레임워크는
서비스 인터페이스
,제공자 등록 API
,서비스 접근 API
로 이루어진다.- 간혹
서비스 제공자 인터페이스
가 쓰이기도 하며,서비스 제공자 인터페이스
가 없다면, 리플렉션을 사용한다. JDBC
에서는서비스 인터페이스
:Connection
제공자 등록 API
:DriverManager.registerDriver
서비스 접근 API
:DriverManager.getConnection
서비스 제공자 인터페이스
:Driver
- 간혹
서비스 접근 API
에게 클라이언트는 원하는 구현체의 조건을 명시한다.- JDBC에서는 어떤 DB를 사용할 것인지, DB 접속 경로, 아이디, 패스워드 등을 입력한다.
- 조건을 명시하지 않으면, 기본 구현체를 하나씩 돌아가며 반환한다.
- 이
서비스 접근 API
가 사실 '유연한 정적 팩토리'의 실체다.
서비스 제공자 인터페이스
가 사용되는 경우, 인스턴스를 생성하는 팩터리 객체를 설명해준다.
- 인터페이스에 직접 객체를 주입하거나 주입받는
브리지 패턴
혹은의존 객체 주입
프레임워크도 강력한 서비스 제공자로 볼 수 있다.
단점들
단점1: 상속에는 public
혹은 protected
생성자가 필요하므로 정적 팩토리 메서드만 제공할 경우, 상속할 수 없다.
- 상속보다는 컴포지션을 사용하는게 낫고(
item 18
), 불변 타입으로 만드는 것이 좋다는 점(item 17
)을 생각하면, 오히려 장점일 수 있다.
단점2: 정적 팩토리 메서드를 다른 개발자가 찾기 어렵다.
- 생성자처럼 API에 명확히 드러나지 않기 때문이다.
- API 문서를 잘 써놓고, 메서드 이름도 규약에 따라 짓는 등 노력할 필요가 있다.
정적 팩토리 메서드 명명방식
from
: 매개변수를 하나 받아서 해당 타입의 인스턴스 반환하는 형변환 메서드Date d = Date.from(instant)
of
: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING)
:EnumSet
반환
enum Cards {
ACE, JACK, QUEEN, KING
}
@Test
public void enumSetTest() {
Set<Cards> cards = EnumSet.of(ACE, JACK);
for (Cards card : cards) {
System.out.println("card = " + card);
}
}
valueOf
:from
과of
의 더 자세한 버전BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance
,getInstance
: 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하진 않는다.StackWalker luke = StackWalker.getInstance(options)
create
,newInstance
:getInstance
와 비슷하지만, 매번 새로운 객체를 반환한다.Object newArray = Array.newInstance(classObject, arrayLen)
getType
:getInstance
와 같으나 생성할 클래스가 아닌 다른 클래스의 팩터리 메서드를 정의할 때 쓴다.Type
이 팩터리 메서드가 반환할 타입이 된다.FileStore fs = Files.getFileStore(path)
newType
:newInstance
와 같으나 마찬가지로 생성할 클래스가 아닌 다른 클래스의 팩터리 메서드를 정의할 때 쓴다. 마찬가지로 _Type
_이 반환할 타입이다.type
:getType
과newType
의 간단한 버전이다.List<Complaint> litany = Collections.list(legacyLitany)
정리
- 무작정
public
으로 생성자를 만들기 전에 정적 팩토리 메서드가 더 좋진 않을까 생각해보자.- 특이한 종류의 생성자라면? -> 정적 팩토리 메서드 패턴으로 이름을 지정하는게 더 좋을 수 있다.
- 싱글톤 혹은 플라이웨이트 패턴을 사용하는게 효율적이라면?
- 유연하게 인터페이스를 반환하고 싶다면?
- 인자에 따라 다른 타입을 반환하고 싶다면?
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2021.12.25 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라 (0) | 2021.12.25 |
이펙티브 자바, 쉽게 정리하기 - item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2021.12.24 |
이펙티브 자바, 쉽게 정리하기 - item2. 생성자에 매개변수가 많다면, 빌더 패턴을 고려하라 (0) | 2021.12.23 |
이펙티브 자바 - 들어가면서... (0) | 2021.12.22 |