본문 바로가기

Dev Book Review/Effective Java 3판

[Effective Java] Item1. 생성자 대신 정적 팩터리 메서드를 고려하라

정적 팩터리 메서드: 해당 클래스의 인스턴스를 반환하는 단순한 정적 메서드

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다.

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명할 수 없다. 이때 정적 팩터리 메서드는 이름을 통해 반환될 객체의 특성을 설명할 수 있다.

public BigInteger(int bitLength, int certainty, Random rnd)

public static BigInteger probablePrime(int bitLength, Random rnd)

위와 같은 경우처럼 probablePrime와 같은 정적 팩터리 메서드가 반환 객체에 대한 정보가 잘 나타난다.

 

public class Car {
	private String name;
    private String brand;
    
    public Car(String name) {
    	this.name = name;
	}
    
//    public Car(String brand) {
//    	this.brand = brand;
//    }
    
    public static Car withBrand(String brand) {
    	Car car = new Car();
        car.brand = brand;
        return car;
    }
}

또한 하나의 시그니처로는 생성자를 하나만 만들 수 있다. 따라서 시그니처가 같은 생성자가 여러 개 필요하다면 정적 팩터리 메서드를 추가하여, 각각의 차이가 잘 드러나는 이름을 지어주면 된다.

2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

1) 불변 클래스(immutable class): 객체 생성 이후 내부의 상태가 변하지 않는 클래스. 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용하는 방식을 통해 불필요한 객체 생성을 피할 수 있다. ex) Boolean.valueOf(value);

2) 플라이웨이트 패턴(flyweight pattern): 공통으로 사용하는 클래스를 만드는 팩토리 클래스를 두고, 사용 클래스의 인스턴스가 팩토리 클래스 내에 있을 시에는 꺼내서 사용하고, 없다면 새로 생성하여 사용하는 패턴.

ex) JAVA에서는 String Pool을 별도로 두어 같은 문자열에 대해 다시 사용될 때 새로운 메모리를 할당하는 것이 아니라 String Pool에 있는지 검사하여 있으면 가져오고, 없다면 새로 메모리를 할당하여 String Pool에 등록한 후 사용하도록 한다.

 

3) 인스턴스 통제(instance-controlled) 클래스: 반복되는 요청에 같은 객체를 반환하는 식으로 구현 가능하다.

 

*인스턴스를 통제하는 이유

- 클래스를 싱글톤으로 만들 수 있다.

- 클래스를 인스턴스화 불가(noninstantiable)로 만들 수 있다.

- 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다. ex) a == b일 때만 a.equals(b)가 성립

인스턴스 통제는 플라이웨이트 패턴의 근간이 되며, 열거 타입은 인스턴스가 하나만 만들어짐을 보장한다.

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

반환할 객체의 클래스를 자유롭게 선택할 수 있다. (엄청난 유연성 제공)

이는 인터페이스 기반 프레임워크의 핵심 기술이다. 

인터페이스 기반 프레임워크: 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용

예를 들어 자바 컬렉션 프레임워크는 총 45개의 유틸리티 구현체를 제공한다고 한다. 하지만 우리는 이러한 구현체를 알 수 없고, 인스턴스화 불가 클래스인 java.util.Collections의 정적 팩터리 메서드를 통해서만 해당 구현체를 얻을 수 있다.

따라서 개발자 입장에서는 알아야할 개념의 개수와 난이도가 줄어들었다. 또한 해당 구현체를 인터페이스만으로 다루게 되며 이는 일반적으로 좋은 습관이라고 한다.

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

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다.

예를 들어 EnumSet 클래스의 정적 팩터리 메서드는 원소의 개수에 따라 반환 타입이 달라진다.

하지만 클라이언트는 이 두 클래스의 존재를 몰라도 된다. 또한 필요에 따라 다른 클래스의 객체를 반환하도록 변경할 수도 있다.

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

이러한 유연함은 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다.

해당 프레임워크에서의 제공자는 서비스의 구현체다. 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제한다. (클라이언트와 구현체의 분리)

 

* service provider framework의 핵심 컴포넌트

1) 서비스 인터페이스(service interface): 구현체의 동작을 정의

2) 제공자 등록 API(provider registration API): 제공자(provider)가 구현체를 등록

3) 서비스 접근 API(service access API): 클라이언트가 인스턴스를 얻을 때 사용. 클라이언트는 원하는 구현체의 조건 명시 가능. 서비스 접근 API가 '유연한 정적 팩터리'의 실체이다.

4) 서비스 제공자 인터페이스(service provider interface): 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명. (없다면 리플렉션 사용)

 

이펙티브 자바에서는 service provider framework의 예시로 JDBC를 설명한다.

Driver driver = new Driver(); // 서비스 제공자 인터페이스
DriverManager.registerDriver(driver); // 제공자 등록 API

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/jdbc?serverTimezone=UTC", "root", "pwd1234");
// Connection: 서비스 인터페이스
// DriverManager.getConnection: 서비스 접근 API

Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM java");

정적 팩터리 메서드의 단점

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

따라서 앞서 얘기한 컬렉션 프레임워크의 유틸리티 구현 클래스들을 상속할 수 없다.

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

컴포지션: 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함하여 메서드를 호출하는 기법.

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

API 문서를 잘 써놓고, 널리 알려진 규약을 따라 메서드 이름을 작성해야 한다.

ex) from, of, valueOf, instance, create, getType, newType, type