Effective Java - item 1
생성자 대신 정적 팩터리 메서드를 고려하라
생성자 대신 정적 팩터리 메서드를 고려하라
클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 이 이외에도 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공하여 인스턴스를 반환할 수 있다. 이 방식을 사용했을 때의 장점과 단점을 비교해보자.
정적 팩터리 메서드의 장점
1. 이름을 가질 수 있다.
생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있어 그 자체만으로 장점이 된다.
Java에서 시그니처(Signature)란?
메서드를 고유하게 식별하는 정보
- 메서드 이름
- 매개변수 타입들의 순서와 개수
반환 타입은 시그니처에 포함되지 않는다
- 기존 생성자 시그니처 방식
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person {
private String name;
private int age;
private String email;
private String phone;
// 생성자들이 많아지면서 혼란스러워짐
public Person(String name) { ... }
public Person(String name, int age) { ... }
public Person(String name, String email) { ... }
public Person(String name, String phone) { ... } // 불가능!
// 매개변수 순서로만 구분해야 함
public Person(String name, int age, String email) { ... }
public Person(String name, String email, int age) { ... } // 헷갈림!
}
- 정적 팩토리 메서드를 사용한 경우
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
30
31
32
33
34
35
36
37
38
public class Person {
private String name;
private int age;
private String email;
private String phone;
private Person(String name, int age, String email, String phone) {
this.name = name;
this.age = age;
this.email = email;
this.phone = phone;
}
public static Person withName(String name) {
return new Person(name, 0, null, null);
}
public static Person withNameAndAge(String name, int age) {
return new Person(name, age, null, null);
}
public static Person withEmail(String name, String email) {
return new Person(name, 0, email, null);
}
public static Person withPhone(String name, String phone) {
return new Person(name, 0, null, phone);
}
public static Person newEmployee(String name, String email) {
return new Person(name, 0, email, null);
}
public static Person newCustomer(String name, String phone) {
return new Person(name, 0, null, phone);
}
}
위와 같이 코드를 생성사의 시그니처를 늘리는 방식에서 정적 팩토리 메서드를 사용한다면 API를 사용하는 개발자가 실수하고 다른 API를 호출하는 일이 발생하지 않을 것이다.
2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
이 덕분에 불변 객체는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 막을 수 있다. 대표적으로 Boolean.valueOf(boolean)과 같은 메서드는 객체가 자주 요청되는 상황이라면 성등을 상당히 끌어올려줄 수 있다. 반복되는 요청에 같은 객체를 반화하는 식으로 통제한다면 싱글턴으로 만들 수도, 인스턴스화가 불가능한 객체로 만들 수 있다.
3. 반환 타입의 하위 타입 객체를 반활할 수 있는 능력이 된다.
이 장점은 반환할 객체의 클래스를 자유롭게 선택할 수 있는 가장 큰 장점이 된다. 예를 들어
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
30
31
32
33
34
35
36
37
// 상위 타입 (부모)
abstract class Animal {
public abstract void sound();
// 정적 팩토리 메서드 - 상황에 맞는 동물 반환
public static Animal create(String environment) {
if ("farm".equals(environment)) {
return new Cow(); // Cow 반환하지만 Animal 타입으로
} else if ("home".equals(environment)) {
return new Dog(); // Dog 반환하지만 Animal 타입으로
} else {
return new Cat(); // Cat 반환하지만 Animal 타입으로
}
}
}
// 하위 타입들 (자식들)
class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("야옹~");
}
}
class Cow extends Animal {
@Override
public void sound() {
System.out.println("음메~");
}
}
이처럼 설계한다면
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
// 환경에 따라 다른 동물이 나오지만, 모두 Animal 타입
Animal homeAnimal = Animal.create("home"); // 실제로는 Dog
Animal farmAnimal = Animal.create("farm"); // 실제로는 Cow
Animal wildAnimal = Animal.create("wild"); // 실제로는 Cat
// 사용할 때는 모두 Animal로 동일하게 사용
homeAnimal.sound();
farmAnimal.sound();
wildAnimal.sound();
}
}
개발자는 사용자의 동작과 함께 모두 같은 Animal 타입이지만 다른 객체를 반환할 수 있게 된다. 사용자는 구체적인 타입을 몰라도 되며, 상황에 따라 최적의 구현체가 선택할 수 있게 된다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반활할 수 있다.
생성자 방법 대신 정적 팩토리 메서드 방법을 사용하게 되면 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없이 반환할 수 있고 클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없어진다.
5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
서비스 제공자 프레임 워크는 3개의 핵심 컴포넌트로 이루어진다.
- 구현체의 동작을 정의하는 서비스 인터페이스
- 제공자가 구현체를 등록할 때 사용하는 제공자 등록 API
- 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API
클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있다. 조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환한다. JDBC로 예를 들어보자
1
2
3
4
5
6
7
8
9
10
11
// Java 8에서 이미 만들어둠 (MySQL 클래스는 아직 없는데도!)
public class DriverManager {
public static Connection getConnection(String url) {
// 나중에 등록될 드라이버들을 찾아서 반환
for (Driver driver : registeredDrivers) {
if (driver.acceptsURL(url)) {
return driver.connect(url);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// MySQL 회사에서 나중에 만든 클래스
public class MySQLDriver implements Driver {
public Connection connect(String url) {
return new MySQLConnection(); // MySQL 전용 연결
}
}
// Oracle 회사에서 나중에 만든 클래스
public class OracleDriver implements Driver {
public Connection connect(String url) {
return new OracleConnection(); // Oracle 전용 연결
}
}
1
2
3
4
5
6
// MySQL 드라이버 등록 (제공자 등록 API)
DriverManager.registerDriver(new MySQLDriver());
// 개발자는 MySQL인지 Oracle인지 몰라도 됨! (서비스 접근 API)
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/mydb");
//
이 예시에서 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/mydb"); 이부분이 바로 정적 팩토리 메서드 부분이다.
정적 팩터리 메서드의 단점
상속의 제약
컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다는 이야기다. 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입을 만들려면 이 제약을 지켜야한다.
정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
생성자처럼 API 설명에 명확하게 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화 할 방법을 알아내야한다. 그래서 명명 방식을 아래와 같이 정의하기도 한다.
- from
- of
- valueOf
- instance, getInstance
- create, newInstance
- getType
- newType
- type