도서/이펙티브 자바

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라

highright96 2021. 5. 28.

정적 팩토리 메서드(Static Factory Method)

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자이다.

하지만 생성자가 아닌 정적 팩토리 메서드로도 인스턴스를 얻을 수 있다.

정적 팩터리 메서드란 private 생성자를 통해 new를 통한 객체 생성을 감추고 static 메서드를 통해 객체 생성을 캡슐화하는 디자인 패턴을 말한다.

위에서 private 생성자를 통해 객체 생성을 막아야 한다고 썼지만 JPA를 사용할 때 Entity의 기본 생성자를 private으로 만들면 절대 안 된다! public 또는 protected를 사용해야 하며 보통 protected를 사용한다.

 

장점

1. 이름을 가질 수 있다.

객체는 생성 목적과 과정에 따라 생성자를 구별해서 사용할 필요가 있다.

하지만 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다.

반면 정적 팩터리 메서드는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.

이해를 돕기 위해 간단한 예제 코드를 작성해보았다.

public class Champion {

    private int hp;
    private int mp;

    Champion(int hp, int mp) {
        this.hp = hp;
        this.mp = mp;
    }

    public static Champion newTopChampion(){
        return new Champion(100, 50); //탑은 hp가 높다
    }

    public static Champion newMidChampion(){
        return new Champion(50, 100); // 미드는 mp가 높다
    }
}

만약 생성자를 사용해 탑이나 미드를 생성한다면 다음과 같을 것이다.

Champion malphite = new Champion(100, 50);
Champion oriana = new Champion(50, 100);

변수명이 없었다면 위에서 말한 것과 같이 매개변수와 생성자 만으로는 챔피언이 탑 챔피언인지 미드 챔피언인지 알아보기 어려웠을 것이다.

 

하지만 정적 팩터리 메서드를 사용한다면 반환될 챔피언의 라인을 알아보기 쉬워진다.

Champion shen = Champion.newTopChampion();
Champion syndra = Champion.newMidChampion();

 

2. 호출될 때마다 새로운 인스턴스를 생성할 필요가 없다.

불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

굳이 일일이 new같은 비싼 연산을 사용할 필요가 없다.

아래는 이해를 돕기 위해 구현한 제비뽑기 코드이다.

public class paper {
    private static final int MIN_NUM = 1;
    private static final int MAX_NUM = 10;

    private static final Map<Integer, DrawingLots> DrawingLotsBox = new HashMap<>();

    static {
        IntStream.range(MIN_NUM, MAX_NUM)
                .forEach(i -> DrawingLotsBox.put(i, new DrawingLots()));
    }

    public int number;

    private paper(){
        number = (int)((Math.random()*10000)%10);
    }

    public static DrawingLots of(int number){
        return DrawingLotsBox.get(number);
    }
}

랜덤 한 숫자가 적힌 paper 객체를 미리 DrawingLotsBox(캐싱)에 넣어두기 때문에 호출할 때마다 paper 객체들을 생성하는 일을 피할 수 있다.

따라서 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능을 끌어올려 준다. 플라이웨이트 패턴(Flyweight pattern)도 이와 비슷한 기법이라 할 수 있다.

 

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

반환할 객체를 유연하게 고를 수 있기 때문에 원하는 객체를 반환할 수 있다. 아래 예시와 같이 상황에 맞게 반환 객체(자신의 하위 타입 중)를 선택할 수 있다.

public class Rank{
    private int score;

    public static Rank of(int score){
        if(score > 90) return new Gold();
        else if(score < 90 && score > 50) return new Sliver();
        else return new Bronze();
    }

    static class Gold extends Rank{}
    static class Sliver extends Rank{}
    static class Bronze extends Rank{}
}

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

 

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

장점 3과 비슷한 의미이다.

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

대표적인 예로 java.util.EnumSet 가 있다.

EnumSet클래스는 public 생성자 없이 오직 정적 팩터리만 제공하는데, 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다.(원소가 64개 이하면 RegularEnumSet, 이상이면 JumboEnumSet)

 

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

 

단점

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

private 생성자만 제공한다면 상속이 불가능하다.

어찌 보면 이 제약은 상속보다 컴포지션을 사용(아이템 18)하도록 유도하고

불변 타입(아이템 17)으로 만들려면 이 제약을 지켜야 한다는 점에서 장점으로 받아들일 수 있다.

 

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

new를 통한 인스턴스화는 모든 개발자들이 알고 있지만 정적 팩터리 메서드 방식 클래스를 인스턴스 화할 방법은 직접 찾아봐야 한다.

따라서 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다.

 

참고

댓글