Effective Java - 1. 생성자 대신 정적 팩터리 메서드 사용

 

Rule 1. 생성자 대신 정적 팩터리 메서드 사용

본 글은 이펙티브 자바 2nd를 읽고 개인적으로 복습하기 위해 작성된 글로 내용상 오류가 있을 수 있습니다. 오류가 있다면 지적 부탁드리겠습니다.

클래스를 통해 객체를 만드는 방법은 아래와 같이 2가지 방법이 있다.

  • public으로 선언된 생성자(constructor)를 이용
  • public으로 선언된 정적 팩터리 메서드(static factory method)를 추가

여기에서 나오는 정적 팩터리 메서드는 디자인 패턴에서 나오는 팩터리 메서드와 다른 개념이다.

클래스를 정의할 때 생성자 대신 또는 생성자와 별도로 정적 팩터리 메서드를 제공함으로써 생기는 장단점은 아래와 같다.

1.1 장점

1.1.1 생성자와 달리 정적 팩터리 메서드에는 이름이 있다.

생성자는 전달되는 인자들은 어떤 객체가 생성되는지 설명하지 못하지만 정적 팩터리 메서드는 이름만 잘 짓는다면 사용하기 쉽고, 클라이언트 코드의 가독성도 높다.

클래스에는 시그니처별로 하나의 생성자만 넣을 수 있기 때문에 이러한 제약을 피하기 위해 인자의 순서를 바꾸기도 한다. 하지만 이 방법은 API사용자 입장에서 코드가 하는 일을 제대로 파악하지 못한다.

반면에 정적 팩터리 메서드는 이름이 있기 때문에 이러한 문제가 발생하지 않는다. 같은 시그니처를 갖는 생성자를 여러개 정의할 필요가 있을 경우 정적 팩터리 메서드로 바꾸고, 메서드 이름을 보면 차이가 드러나게 작명하도록 하자.

1.1.2 생성자와 달리 호출할 때마다 새로운 객체를 생성할 필요가 없다.

변경 불가능 클래스라면 이미 만들어둔 객체를 활용할 수 있고, 만든 객체를 캐시해놓고 재사용하여 객체가 불필요하게 거듭 생성되는 일을 피할 수 있다. 동일한 객체가 요청되는 일이 잦고, 특히 객체를 만드는 비용이 클 때 적용하면 성능을 크게 개선할 수 있다.

같은 객체를 반복해서 반환할 수 있기 때문에 어떤 시점에 어떤 객체가 얼마나 존재할지를 정밀하게 제어할 수 있다. 이러한 기능을 가진 객체를 개체 동제 클래스라고 한다.

개체 통제 클래스를 작성하는 이유는 아래와 같다.

  • 싱글턴 패턴을 따르도록 할 수 있다.
  • 객체 생성이 불가능한 클래스를 만들 수 있다.
  • 변경이 불가능한 클래스의 경우 두 개의 같은 객체가 존재하지 못하도록 할 수도 있다.
    • a == b일 때만 a.equals(b)가 참이 되도록 만들 수 있다.
    • equals()대신 ==를 사용할 수 있기 때문에 성능이 향상된다.

1.1.3 생성자와 달리 반환값 자료형의 하위 자료형 객체를 반환할 수 있다.

반환되는 객체의 클래스를 유연한게 결정할 수 있다. 이러한 유연성을 활용하면 pulic으로 선언되지 않은 클래스의 객체를 반환하는 API를 만들 수 있다. 구현 세부사항을 감출 수 있기 때문에 아주 간결한 API가 가능해진다.

대표적인 예로 자바 컬렉션 프레임워크가 있다. 32개의 컬렉션 인터페이스 구현체가 있는데 이것들은 변경이 불가능한 컬렉션과 동기화된 컬레션 등이다. 이 구현체는 전부 java.util.Collections 라는 객체 생성 불가능 클래스의 정적 팩터리 메서드를 통해 이용하는데 반환되는 객체의 실제 클래스는 public이 아니다.

1.1.4 형인자 자료형(parameterized type) 객체를 만들 때 편하다.

Map<String, List<String>> m = new HashMap<String, List<String>>();

위의 코드와 같이 클래스의 생성자를 호출할 때, 형인자가 명백하더라도 반드시 인자로 형인자를 전달해야한다. 그래서 보통 연달아서 두번 사용하게 된다. 자료형 명세를 중복하면 형인자가 늘어남에 따라 길고 복잡한 코드가 만들어진다.

정적 팩터리 메서드를 사용하면 컴파일러가 형인자를 스스로 알아내도록 할 수 있다. 이러한 기법을 자료형 유추(Type interface)라고 한다.

public static <K, V> hashMap<K, V> newInstance() {
  return new HashMap<K, V>();
}

위와 같이 HashMap의 클래스가 아래의 제네릭 정적 팩터리 메서드를 제공한다고 가정한다면 선언문을 좀더 간결하게 작성할 수 있다.

Map<String, List<String>>() m = HashMap.newInstance();

자바 1.7부터 생성자를 호출할 때도 자료형 유추를 사용할 수 있게 되었다.

Map<String, List<String>>() m = new HashMap<>();

1.2 단점

1.2.1 pulbic이나 protected로 선언된 생성자가 없으므로 하위 클래스를 만들 수 없다.

public 정적 팩터리 메서드가 반환하는 비-public 클래스도 마찬가지인데 예를 들어 자바 컬렉션 프레임워크에 포함된 기본 구현 클래스들의 하위클래스는 만들 수 없다.

이러한 이유는 상속(inheritance) 대신 구성(composition) 기법을 쓰도록 장려하기 때문이다.

1.2.2 정적 팩터리 메서드가 다른 정적 메서드와 확연히 구분되지 않는다.

API문서를 보면 생성자는 다른 메서드와 구분이 되지만 정적 팩터리 메서드는 구분이 되지 않는다. 그래서 클래스나 인터페이스 주석을 통해 정적 팩터리 메서드임을 알리거나 이름을 지을 때 조심하는 수 밖에 없다.

보통 아래와 같이 정적 팩터리 메서드 이름을 사용한다.

  • valueOf : 인자로 주어진 값과 같은 값을 갖는 객체를 반환할 때, 형변환 메서드
  • of : valueOf를 더 간단하게 쓴 것
  • getInstance : 인자에 기술된 객체를 반환, 인자와 같은 값을 갖지 않을 수도 있음, 싱글톤인 경우 인자 없이 항상 같은 객체 반환
  • newInstance : getInstance와 같지만 호출할 때마다 다른 객체 반환, 새로운 객체 생성
  • getType : getInstance와 같지만, 반환될 객체의 클래스와 다른 클래스에 팩터리 메서드가 있을 때 사용, Type은 팩터리 메서드가 반환할 객체의 자료형
  • newType : newInstance와 같지만 반환될 객체의 클래스와 다른 클래스에 팩터리 메서드가 있을 때 사용, Type은 팩터리 메서드가 반환할 객체의 자료형

1.3 요약

정적 팩터리 메서드와 public 생성자의 용도는 다르기 때문에 차이와 장단점을 이해하는 것이 중요하다. 정적 팩터리 메서드가 효과적인 경우가 많으니 무턱대고 public 생성자만 만드는 것을 삼가하는 것이 좋다.