Post

Effective Java - item 1

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

Effective Java - item 1

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

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 이 이외에도 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공하여 인스턴스를 반환할 수 있다. 이 방식을 사용했을 때의 장점과 단점을 비교해보자.

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다.

생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있어 그 자체만으로 장점이 된다.

Java에서 시그니처(Signature)란?
메서드를 고유하게 식별하는 정보

  1. 메서드 이름
  2. 매개변수 타입들의 순서와 개수

반환 타입은 시그니처에 포함되지 않는다

  • 기존 생성자 시그니처 방식
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
This post is licensed under CC BY 4.0 by the author.