Home 생성자 대신 정적 팩토리 메서드를 고려하자.
Post
Cancel

생성자 대신 정적 팩토리 메서드를 고려하자.

보통 개발에 있어 public 생성자를 자주 사용하게 된다. 이펙티브 자바에서는 이러한 public 생성자보다 정적 팩토리 메서드를 사용하면 행복한 상황들이 일어난다고 한다.

정적 팩토리 메서드

정적 팩토리 메서드는 클래스의 인스턴스를 반환하는 생성자와 동일한 역할을 하는 단순 static method를 제공하는 것을 말한다.

1
2
3
public static Instance method() {
	return new Instance();
}

장점

이름을 가질 수 있다.

1
2
3
4
5
6
7
8
9
10
public class Product {
	private String name;
	private int stock;

	public Product(String name, int stock) {
		this.name = name;
		this.stock = stock;
	}
	/...
}

위와 같은 단순한 public 생성자를 보면, 생성자를 넘기는 매개변수와 생성자 자체만으로 반환되는 객체의 특성을 정확히 설명할 수 없다. 정적 팩토리를 적용했을 경우를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Product {
	private String name;
	private int stock;

	private Product(String name, int stock) {
		this.name = name;
		this.stock = stock;
	}
	
	public static Product getDefaultInstance() {
		return new Product("default", 0);
	}

	public static Product getComputerInstance() {
		return new Product("computer", 10);
	}

	/...
}

이렇게 이름을 지어줌으로써 특성을 제대로 설명할 수 있게 된다. 만약 기본 생성자를 사용했다면 new Product에 전달하는 매개변수를 통해 추측했어야 할 것이다.

이제 클라이언트는 computer를 얻고 싶다면 computer를 얻는 정적 팩토리 메서드를 호출하면 된다.

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

불변 클래스는 인스턴스를 미리 만들거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 방식으로 불필요한 객체 생성을 막을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Product {
	private final String name;
	private final int stock;
	private static Product instance = new Product("default", 0);

	private Product(String name, int stock) {
		this.name = name;
		this.stock = stock;
	}

	public static Product getInstance() {
		return instance;
	}
	/...
}

반복되는 요청에 같은 객체를 반환하는 방식을 통해 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아있게 할 지 통제할 수 있다. 이것을 인스턴스 통제 클래스라고 하는데 이를 통해 싱글턴으로 만들 수도, 인스턴스화 불가로 만들 수도 있다. 또한 불변 값 클래스에서 동치인 인스턴스가 하나임을 보장할 수 있다.

반환 타입의 하위 타입 객체를 반환할 수 있다.

1
2
3
4
5
6
7
8
public class Book extends Product {
	private final String author;

	protected Book(String author) {
		super("book", 0);
		this.author = author;
	}
}

API를 만들 때 이러한 유연성을 이용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.

API를 작게 유지하는 것의 하나의 예시로 자바 컬렉션 프레임워크를 들 수 있다.

자바 컬렉션 프레임워크는 핵심 인터페이스들에 다양한 기능을 덧붙인 총 45개의 유틸리티 구현체를 제공하는데, 대부분 구현체를 단 하나의 인스턴스화 불가 객체인 java.utill.Collections에서 정적 팩터리 메서드를 통해 얻도록 했다.

컬렉션 프레임워크는 이 45개 클래스를 공개하지 않기 때문에, API 외관을 훨씬 작게 만들 수 있었다. API가 작아진것은 개발자가 익혀야하는 개념과 난이도를 낮추었고, 인터페이스만을 알면 되기 때문에 실제 구현 클래스를 클라이언트는 몰라도 사용할 수 있게 됐다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Product {
	private final String name;
	private final int stock;
	private static Product instance = new Product("default", 0);
	
	protected Product(String name, int stock) {
		this.name = name;
		this.stock = stock;
	}

	public static Book getBookInstance() {
		return new Book("작가");
	}
	
	public static Computer getComputerInstance() {
		return new Computer("i9");
	}

	public static Product getInstance(String type) {
		switch (type) {
			case "computer":
				return getComputerInstance();
			case "book":
				return getBookInstance();
			default:
				return instance;
		}
	}
}

반환 타입의 하위 타입이기만 한다면 어떤 클래스의 객체를 반환해도 상관없다. 심지어 추후 변경 시 또 다른 클래스의 객체를 반환해도 된다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Jdbc {
	Map<String, Driver> DbDrivers = new HashMap<>();

	static {
		DbDrivers.put("mysql", new MysqlDriver());
		DbDrivers.put("oracle", new OracleDriver());
		DbDrivers.put("postgresql", new PostgresqlDriver());
		DbDrivers.put("redis", new RedisDriver());
		/...
	}
	
	public static Connection getConnection(String dbName) {
		Driver dbDriver = DbDrivers.get(dbName);
		if (dbDriver == null) {
			throw new IllegalArgumentException("잘못된 DB 이름입니다.");
		}
		return dbDriver.getConnection();
	}
}

getConnection이 정적 팩토리 메서드라고 볼 수 있다. Connection을 얻을 DB가 예를들어 aSQL 이라고 하면 aSQL이 정의되어 있지 않아도 위와 같이 정적 팩토리 메서드는 작성할 수 있는 것이다.

단점

상속에서의 문제

상속을 하기 위해서는 public이나 protected 생성자가 필요하다. 정적 팩토리 메서드만 제공한다면 하위 클래스를 만들 수 없다. (private 생성자를 사용하기 때문)

위에서 말한 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다. 이 제약은 상속보다 컴포지션을 사용하도록 유도하고, 불변타입으로 만들기 위해 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수 있다.

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

생성자와 같이 API 설명에 명확히 드러나지 않아 사용자는 정적 팩토리 메서드 방식 클래스를 인스턴스화 할 방법을 알아내야 한다.

그렇기 때문에 널리 알려진 규약을 따라 짓는 방식으로 문제를 완화하도록 한다.

규약

  • from
1
Date d = Date.from(instant);

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

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

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

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

from과 of의 더 자세한 버전.

  • instance / getInstance
1
StackWalker luke = StackWalker.getInstance(options);

(매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는 메서드 네이밍

  • create / newInstance
1
Object newArray = Array.newInstance(classObject, arrayLen);

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

  • getType
1
FileStore fs = Files.getilesStore(path);

getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 사용한다. Type은 팩토리 메서드가 반환할 객체의 타입이다.

This post is licensed under CC BY 4.0 by the author.