객체 생성과 파괴(Item 1)
Item 1. 생성자 대신 정적 팩토리 메서드를 고려하라
생성자란?
자바에서 생성자는 객체를 생성하는 유일한 문법이다. 객체를 생성한다는 것은 런타임에 Heap 메모리 영역에 객체를 올린다는 뜻이다.
정적 팩토리 메서드란?
정적 팩토리 메서드(Static Factory Method)란, 문자 그대로 정적인 팩토리 메서드를 의미한다. 즉, static 예약어를 달고 있는 객체 생성 메서드를 정적 팩토리 메서드라고 부른다.
생성자의 단점
이름을 지어줄 수 없다
메서드나 변수는 이름만으로도 사용자에게 정보를 제공할 수 있는데, 생성자는 문법상 이름을 지어줄 수 없다.
같은 타입의 매개변수들이 구분되지 않는다
생성자는 이름이 없으니 매개변수로 서로 구분해야 하는데, 두 생성자의 매개변수 타입이 같다면 설령 그 매개변수가 서로 다른 의미의 매개변수라고 할지라도 컴파일 에러가 발생한다.
다음 코드를 보자.
public class Order {
private int orderNum;
private int tableNum;
public Order(int orderNum) { // Compile Error!
this.orderNum = orderNum;
}
public Order(int tableNum) { // Compile Error!
this.tableNum = tableNum;
}
}
위 객체는 orderNum
필드와 tableNum
필드를 가지고 있다. 생성자로 두 값을 따로 받고 싶지만 생성자 문법상 불가능하다.
객체 생성을 컨트롤 할 수가 없다
생성자는 반드시 새로운 인스턴스를 반환하기 때문에 생성자를 사용할 수 있는 곳에서는 얼마든지 해당 객체의 인스턴스를 무한정 만들어낼 수 있다.
반드시 해당 객체의 타입만 반환한다
생성자는 해당 객체의 인스턴스를 생성하는 문법이기 때문에 반드시 그 객체의 타입으로만 반환받을 수 있다.
정적 팩토리 메서드의 장점
이름을 지어줄 수 있다
정적 팩토리 메서드는 메서드이기 때문에 이름을 가질 수 있다. 따라서, 이름에 정보를 담을 수 있다.
매개변수 타입이 같아도 구분할 수 있다
매개변수 타입이 같은 두 메서드라고 할지라도 이름을 통해 구분 지어줄 수 있다.
인스턴스를 컨트롤 할 수 있다
인스턴스를 어딘가에 미리 만들어 둔 후에 해당 인스턴스를 반환하도록 하여 불필요한 객체 생성을 막을 수 있다. 이 특징으로 싱글톤 패턴을 구현할 수 있게 된다(생성자로는 싱글톤 패턴을 구현할 수 없다).
또한, 매개변수에 따라 각기 다른 인스턴스를 반환하도록 구현할 수도 있다. (ex. Boolean
클래스의 valudOf
메서드)
반환 타입을 유동적으로 지정할 수 있다
해당 클래스 타입이 아니더라도 반환 타입을 지정해줄 수 있으며 인터페이스로 반환할 수도 있다. 더욱 중요한 것은 상황에 따라 각기 다른 하위 타입의 객체를 반환할 수도 있다.
다음 코드를 보자.
public interface List<E> {
static <E> List<E> of(E... elements) {
switch (elements.length) {
case 0:
var list = (List<E>) ImmutableCollections.EMPTY_LIST;
return list;
case 1:
return new ImmutableCollections.List12<>(elements[0]);
case 2:
return new ImmutableCollections.List12<>(elements[0], elements[1]);
default:
return ImmutableCollections.listFromArray(elements);
}
}
}
자바에서 제공하는 List
인터페이스의 of
메서드는 인터페이스 타입을 반환하고 있는 정적 팩터리 메서드이다.
위 메서드는 매개변수의 개수에 따라 각기 다른 구현체를 반환한다. 이 특징으로 인해 정적 팩토리 메서드는 다형성을 더욱 활용할 수 있게 된다.
정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다
말이 좀 어려운데 코드를 바로 보면서 이해해보자.
public class GameManager {
private GameManager() {
}
public static Optional<Game> findGame(String gameName) {
Objects.requireNonNull(gameName, "Argument 'gameName' is null");
ServiceLoader<Game> loader = ServiceLoader.load(Game.class);
for (Game game : loader)
if (gameName.equalsIgnoreCase(game.getName())) return Optional.of(game);
return Optional.empty();
}
}
위 코드에서 GameManager
클래스는 findGame
정적 메서드를 통해 Game
인터페이스를 제공한다.
하지만 그 어디에도 구현체는 찾아볼 수 없는데, 이것이 정적 팩토리 메서드의 능력이다.
실제로 내가 저 코드를 구현할 당시에는 구현체가 아예 없었다. 그래서 해당 메서드의 반환 객체가 빈 Optional
객체였다.
위 정적 팩토리 메서드는 구체적인 클래스가 없어도 작성할 수 있다. 즉, 사용과 구현이 완벽하게 분리된다.
구현은 저 팩토리 메서드를 사용할 사용자에게 미룬다. 물론 그 사용자도 나였지만… 나는 구현 클래스를 다른 모듈에서 나중에 구현하였다. 책에서는 이 특징을 ‘서비스 제공자 프레임워크’의 근간이 된다고 설명한다.
정적 팩토리 메서드의 단점
상속을 하기 어렵다
정적 팩토리 메서드를 통해 인스턴스를 통제하고자 할 때는 보통 생성자를 막아두는 편이다.
생성자를 private
지시자로 선언하게 되면 하위 클래스에서 생성자를 참조할 수 없기 때문에 상속을 받을 수 없게 된다.
즉, 해당 클래스에 상속을 허용하고 싶다면 생성자를 public
혹은 protected
지시자로 선언해야만 한다.
상속 대신 Composite 관계를 활용
생성자를 열어주고 싶지 않으면 composite 관계를 사용해 볼 수 있다. 해당 클래스를 필드로 참조하여 사용하는 것이 대표적인 경우라고 볼 수 있다.
생성자를 열어주는 경우
생성자를 열어주는 경우도 있다.
일예로 List
인터페이스는 new ArrayList<>()
생성자를 통해 객체를 만들 수 있지만,
of
정적 팩토리 메서드를 통해 객체를 생성할 수도 있다.
찾기 어렵다
정적 팩토리 메서드는 메서드의 일부로 치부되기 때문에 메서드가 많은 클래스라면 찾기 어려울 수 있다. 생성자는 Javadoc 페이지나 IntelliJ IDEA에서 제공하는 클래스 구조 UI에서 따로 분류되거나 맨 위에 표시되기 때문에 찾기가 쉽지만, 정적 팩토리 메서드는 그렇지 않기 때문에 어떻게 객체를 생성해야 하는지 일일이 찾아야 한다.
그래서 이를 찾기 쉽게 정적 팩토리 메서드는 일종의 명명 규칙이 생겼는데, 이펙티브 자바 저자는 다음과 같은 명명 규칙을 소개한다.
정적 팩토리 메서드 명명 규칙
from
매개변수를 하나 받아서 해당 타입의 인스턴스를 반환
method: Date from(Instant instant)
example: Date d = Date.from(instant);
of
여러 매개변수를 받아 적합한 타입의 인스턴스를 반환
method: <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
example: Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf
from
과 of
의 더 자세한 버전
method: BigInteger valueOf(long val)
example: BigInteger prime = BigInteger.valudOf(Integer.MAX_VALUE);
instance 혹은 getInstance
(매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
method: StackWalker getInstance(Option option)
example: StackWalker luke = StackWalker.getInstance(options);
create 혹은 newInstance
instance
혹은 getInstance
와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
method: Object newInstance(Class<?> componentType, int length)
example: Object newArray = Array.newInstance(classObject, arrayLen);
getType
getInstance
와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다.
“Type”은 팩토리 메서드가 반환할 객체의 타입이다.
method: FileStore getFileStore(Path path)
example: FileStore fs = Files.getFileStore(path);
newType
newInstance
와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다.
“Type”은 팩토리 메서드가 반환할 객체의 타입이다.
method: BufferedReader newBufferedReader(Path path)
example: BufferedReader br = Files.newBufferedReader(path);
type
getType
과 newType
의 간결한 버전
method: <T> ArrayList<T> list(Enumeration<T> e)
example: List<Complaint> litany = Collections.list(legacyLitany);
[참고]
백기선, inflearn 강의 이펙티브 자바 완벽 공략 1부
Joshua Bloch, 이펙티브 자바