study/java

Effective JAVA 2장 객체 생성과 파괴 - 아이템 #1

dev.s 2019. 8. 19. 00:50

아이템 1 : 생성자 대신 정적 팩터리 메서드를 고려하라

클래스의 인스턴스를 얻는 전통적인 수단은 public constructor.
생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다. 그 클래스의 인스턴스를 반환하는 단순 정적 메서드.
다음 코드는 기본 타입인 boolean 값을 받아 Boolean 객체를 참조로 변환해준다.

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

생성자보다 정적 팩터리 메서드가 좋은 점

  • 이름을 가질 수 있다.
    • 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 정적 팩터리 메서드는 이름을 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
    • 하나의 시그니처로는 생성자 하나만 만들 수 있다. 입력 매개변수들의 순서를 다르게 한 생성자를 새로 추가하는 식으로 이 제한을 피해볼 수도 있지만, 각 생성자가 어떤 역할을 하는 지 정확히 기억하기 어려워 엉뚱한 것을 호출하는 실수를 할 수 있다. 코드를 읽는 사람도 클래스 설명 문서를 찾아보지 않고는 의미를 알 수 없다.
    • 정적 팩터리 메서드는 이름을 잘 지으면 위와 같은 문제점을 해결할 수 있다. 생성자가 여러개 필요할 것 같으면 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자.
  • 호출될 때마다 인스턴스를 새로 생성할 필요가 없다.
    • 불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용할 수 있다. 불필요한 객체 생성을 피할 수 있다.
    • 생성 비용이 큰 같은 객체가 자주 요청되는 상황이라면 성능을 끌어올려 준다. 플라이웨이트 패턴(Flyweight pattern) 과 비슷한 기법이다.
    • 인스턴스 통제(instance-controlled) 클래스. 인스턴스를 통제하면 클래스를 싱글턴(singleton)으로 만들 수도, 인스턴스화 불가(noninstantiable)로 만들수도 있다. 또한 불변 값 클래스에서 동치인 클래스가 단 하나 임을 보장할 수 있다. 인스턴스 통제는 플라이웨이트 패턴의 근간이 되며 열거 타입은 인스턴스가 하나만 만들어짐을 보장한다.
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    • 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 유연성. 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API 를 작게 유지할 수 있다. 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이다.
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다. EnumSet 클래스는 public 생성자 없이 오직 정적 팩터리만 제공하는데, 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다. 원소가 64개 이하면 long 변수 하나로 관리하는 RegularEnumSet 인스턴스를, 65개 이상이면 long 배열로 관리하는 JumboEnumSet 의 인스턴스를 반환한다.
  • 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • 서비스 제공자 프레임워크를 만드는 근간이 된다. 대표적으로 JDBC(Java Database Connectivity) 가 있다. 서비스 제공자 프레임워크에서의 제공자는 서비스의 구현체다.
    • 서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다. 서비스 인터페이스, 제공자 등록 API(provider registration API), 서비스 접근 API(service access API). 클라이언트는 서비스 접근 API 를 사용할 때 원하는 구현체의 조건을 명시할 수 있다.

단점

  • 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    • 유틸리티 구현 클래스들은 상속할 수 없다. 어찌보면 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수 있다.
  • 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
    • 생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다. APi 문서를 잘 써놓고, 메서드 이름도 널리 알려진 규약에 따라 짓는 식으로 문제를 완화해야 한다. 다음은 흔히 사용하는 명명방식이다.

from : 매개 변수 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드

Date d = Date.from(instant);

of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드

Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

valueOf : from과 Of의 더 자세한 버전

BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

instacne or getInstance : 매개변수로 명시한 인스턴스를 반환하지만 같은 인스턴스임을 보장하지는 않는다.

StackWalker luke = StackWalker.getInstance(oprions);

create or newInstance : instance or getInstance 와 같지만 매번 새로운 인스턴스를 생성해 반환함을 보장한다.

Object newArray = Array.newInstance(classObject, arrayLen);

getType : getInstance 와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.

FileStore fs = Files.getFileStore(path);

newType : newInstance 와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.

BufferedReader br = Files.newBufferedReader(path);

정리

static factory method 와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇지만 static factory method 가 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하는 습관이 있다면 고치자!