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

OH JU HYEON·2022년 7월 13일
1

Java

목록 보기
1/6
post-thumbnail

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

아이템 1

클래스의 인스턴스를 얻는 방법으로 주로 public 생성자를 사용하지만 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.

정적 팩터리 메서드가 생성자보다 좋은 5가지 이유는 아래와 같다.

  1. 이름을 가질 수 있다.
  2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
  3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

5가지 이유에 대해 천천히 살펴보도록 한다.

참고 : 아래에서 사용되는 예시는 따로 만들어 본 예시입니다.

✔ 이름을 가질 수 있다.

생성자에 전달되는 파라미터와 그 자체로는 반환될 객체의 특성을 설명하지 못 한다. 하지만 정적 팩터리는 이름을 갖을 수 있기 때문에 객체의 특성을 묘사할 수 있다.

그리고 생성자는 이름의 의미를 더 잘 설명하는 것이 불가능하다. 하나의 시그니처로는 하나의 생성자만 만들 수 있기 때문이다. 다른 방법으로 입력 파라미터의 순서를 다르게 해서 생성자를 새로 추가할 수 있지만 매우 좋지 못 한 방법이다.

아래에서 예시를 통해 책에서 체크하는 포인트에 대해 코딩을 해 보면서 이해하면 좋을 것 같다.

생성자

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

간단한 생성자 예시를 만들어 봤다. 익숙한 User 클래스를 만들었고 nameage를 파라미터로 갖는 생성자 User를 만들어 주었다. 이렇게 만든 생성자를 사용하려면 아래와 같이 진행해 줘야 한다.

public class Main {

    public static void main(String[] args) {

        User user = new User("오주현", 27);
    }
}

new 키워드로 인스턴스를 생성하고 User()에 파라미터 값을 넣어줘야 한다.

간단한 팁을 적자면 인텔리제이를 사용하면 ctrl + P 단축키로 파라미터로 어떤 값을 받는지 체크할 수 있다.

지금의 예시는 User를 만들려는 것 임을 알 수 있지만 조금 더 복잡해지면 이 생성자로 생성되는 객체가 무엇인지 알기 어려워 질 수 있다고 하는데 그것을 책에서 경계하고 정적 팩터리 메서드를 통해 해결하려고 하는 것 같다.

이름을 더 잘 설명하려는 잘못된 생성자

책에서 생성자는 하나의 시그니처로 하나의 생성자만 만들 수 있기 때문에 이름의 의미를 더 잘 설명하는 것이 힘들다고 했다. 내가 생각했을 때 이런 의미가 아닌가 싶다.

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User(int age, String name) {
        this.age = age;
        this.name = name;
    }

완전 똑같은 순서의 파라미터를 갖는 생성자는 만들 수 없다. 그리고 클래스 명과 다른 이름의 생성자 또한 만들 수 없다.

편법으로 파라미터의 순서를 바꿔주면 되는데 이런 것이 매우 안 좋은 것이라고 책에서는 말 한다.

만약 여기서 내가 age를 먼저 받아오는 생성자를 사용하려고 한다면 아래와 같이 코딩을 해 줘야 한다.

public class Main {

    public static void main(String[] args) {

        User users = new User(27, "오주현");

    }
}

이렇게 하면 age를 먼저 파라미터로 받는 생성자를 사용할 수 있다. 그럼 여기서 이어서 name을 파라미터로 먼저 받는 생성자를 만들어주면 어떻게 되나?

public class Main {

    public static void main(String[] args) {

        User users = new User(27, "오주현");
        User user = new User("오주현", 27);
    }
}

이렇게 될 것이다. 지금의 예시는 객체가 단순한 주제이기도 하고 파라미터 또한 적고 단순해서 딱 보면 알겠지만 객체의 주제가 복잡하고 파라미터 또한 많다고 가정하면 어떤 생성자가 어떤 객체를 생성하려는 것인지, 어떤 것을 먼저 받아오려고 하는 것인지, 어떤 목적으로 생성하려는 것인지 알기 매우 어렵다.

아래에서 이런 불편함을 정적 팩터리 메서드로 해결하는 방안에 대해 알아보면서 책에서 말 하는 정적 팩터리 메서드의 장점을 이해하자.

정적 팩터리 메서드로 해결

먼저 정적 팩터리 메서드의 예시를 먼저 들어보면 아래와 같다.

public class User {
    private String name;
    private int age;

    public static User getUser(String name, int age) {
        User user = new User();
        user.name = name;
        user.age = age;
        return user;
    }
}

User 클래스에서 static으로 객체를 메모리에 등록하므로 따로 new 키워드를 통해 인스턴스를 생성할 필요가 없어진다. 그리고 객체의 이름을 설정할 수 있서 무엇을 생성하려고 하는지 한 눈에 알 수 있다.

public class Main {

    public static void main(String[] args) {

        User user = User.getUser("오주현", 27);
    }
}

사용도 간단하다. new 키워드 없이 사용이 가능한 모습을 볼 수 있다.

위에 이름을 더 잘 설명하려는 잘못된 생성자의 문제를 정적 팩터리 메서드의 예시에서 확인할 수 있듯이 정적 팩터리 메서드는 이름을 정해줄 수 있기 때문에 아래와 같은 방법으로 해결이 가능하다.

public class User {
    private String name;
    private int age;

    public static User getUserNameFirst(String name, int age) {
        User user = new User();
        user.name = name;
        user.age = age;
        return user;
    }
    public static User getUserAgeFirst(int age, String name) {
        User user = new User();
        user.name = name;
        user.age = age;
        return user;
    }
}

name을 먼저 파라미터로 받는 메서드의 이름은 getUserNameFirst로 정해주었고, age를 먼저 파라미터로 받는 메서드의 이름은 getUserAgeFirst로 지어주었다. 한 눈에 확인할 수 있게 되었고 사용은 아래와 같이 사용한다.

public class Main {

    public static void main(String[] args) {

        User userNameFirst = User.getUserNameFirst("오주현", 27);
        User userAgeFirst = User.getUserAgeFirst(27, "오주현");
    }
}

어떤 것이 먼저인지 한 눈에 확인이 가능해졌다. 이게 정적 팩터리 메서드의 장점이고 책에서 말 하는 생성자의 불편한 점을 커버해줄 수있는 방안이지 않나 싶다.

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

불변 클래스(아이템 17, 추후에 아이템 17을 작성하게 된다면 링크를 걸어두겠다.) 덕분에 인스턴스를 미리 만들거나 새로 생성한 인스턴스를 캐싱해 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

예시

// new 키워드로 인스턴스 생성, 생성자 사용
User users = new User(27, "오주현"); 

// 정적 팩터리 메서드 구현할 때 생성한 인스턴스 캐싱
User userNameFirst = User.getUserNameFirst("오주현", 27);

위에서 사용 예시를 참고하면 생성자와 정적 팩터리 메서드의 사용 방법은 조금 다르다.

생성자는 new 키워드를 통해 인스턴스를 그때 그때 생성하는 반면, 정적 팩터리 메서드는 구현할 때 static을 통해 인스턴스를 생성하기 때문에 생성된 인스턴스를 사용할 때 가져오기만(캐싱)하면 되는 것이다.

팩터리 메서드와 비슷한 기법

특히 생성 비용이 큰 객체가 자주 요청되는 상황이라면 이런 정적 팩터리 메서드가 성능을 상당히 끌어올려준다고 한다. Flyweight Pattern(플라이웨이트 패턴)도 이와 비슷한 기법이라고 할 수 있다.

반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 인스턴스를 통제할 수 있는데 이것을 인스턴스 통제(instance controlled) 클래스라고 한다.

인스턴스를 통제하는 이유

인스턴스를 통제하면 클래스를 싱글턴(아이템3)으로 만들 수도 있고, 인스턴스화 불가(아이템4)로 만들 수도 있다. 또, 불변 값 클래스(아이템17)에서 동치인 인스턴스가 단 하나 뿐임을 보장할 수도 있다.

인스턴스 통제는 플라이웨이트 패턴의 근간이 되고 열거 타입(아이템34)은 인스턴사가 하나만 만들어 지는 것을 보장한다고 책에서 안내하고 있다.

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

내용

반환 타입의 하위 타입 객체를 반환할 수 있는 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 유언성을 지닌다. API를 만들 때 이것을 응용해 구현 클래스를 공개하지 않으면서 객체를 반환할 수 있게 되면서 API를 작게 유지할 수 있다.

책에서는 위와 같이 설명하고 있는데 이것에 대한 예시로는 인터페이스가 있다. 인터페이스에 대한 내용은 인터페이스 기반 프레임워크(아이템 20)에서 다루는 핵심 기술이라고 한다.

간단히 인터페이스에 대해 설명하자면 자바에서 인터페이스는 다른 클래스를 작성할 때 기본이 되는 틀을 제공하면서, 다른 클래스 사이의 중간 매개 역할까지 담당하는 추상 클래스를 말 한다.

책에서 자세한 내용은 자바 8 이전과 이후로 나눠 설명하는데 마찬가지로 8의 전과 후로 나눠 체크해 보자.

자바 8 이전에는..

자바 8 이전에는 인터페이스에 정적 메서드를 선언할 수 없었다. 때문에 인스턴스화 불가인 Companion class(동반 클래스)를 만들어 그 안에 정의하는 것이 관례였다. 이에 대한 예시로 java.util.Collections가 있다고 한다.

java.util.Collections는 핵심 인터페이스에 수정 불가, 동기화 기능을 붙인 총 45개의 유틸리티 구현체를 정적 팩터리 메서드를 통해 얻도록 한 클래스이다.

컬렉션 프레임워크는 java.util.Collections 때문에 API를 작게 만들 수 있었고 API를 익히기 위한 개념의 수와 난이도도 낮출 수 있었다고 한다. 그리고 정적 팩터리 메서드를 사용하는 클라이언트는 얻은 객체를 인터페이스만으로 다루게 되므로 결론적으로 좋다고 볼 수 있게 된다.

자바 8 이후에는..

이때부터 인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀리게 되었다. 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 사라졌고 동반 클래스에서 사용하던 public 정적 멤버들 또한 그냥 인터페이스 자체에 넣어버림으로써 해결하게 되었다.

하지만 자바 8에서도 public 정적 멤버만 허용되기 때문에 private는 여전히 별도의 클래스에 두어야 한다. 자바 9에서는 private까지 허락하지만 정적 필드와 멤버 클래스는 여전히 public이어야 한다고 한다.

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

내용

핵심 내용은 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하던 상관이 없다는 것이다.

책에서는 EnumSet 클래스를 예를 들었는 EnumSet은 정적 팩터리 메서드만 제공하고 있다. 이때 Open JDK는 원소의 수에 따라 RegularEnumSet(원소의 수가 64개 이하일 경우)과 JumboEnumSet(원소의 수가 65개 이상인 경우)을 반환해 주어 관리하게 된다.

이처럼 정적 팩터리 메서드를 통해 유연성을 가질 수 있다.

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

내용

위와 같은 유연함은 서비스 제공자 프레임워크(Service Provider Framework)의 근간이 되는데 이 서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 구성된다.

  1. service interface : 구현체의 동작을 정의
  2. provider registration API : 제공자가 구현체를 등록할 때 사용하는 제공자 등록 API
  3. service access API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API

서비스 접근 API는 클라이언트가 구현체의 조건을 명시할 수 있지만 명시하지 않을 경우 기본 구현체를 반환하거나 지원하는 구현체를 번갈아가며 반환하게 된다. 이 서비스 접근 API가 서비스 제공자 프레임워크의 정적 팩터리의 실체라고 할 수 있다.

참고

인터페이스
Effective Java 3/E

profile
읽기만 해도 이해가 되는 글을 쓰기 위해 노력합니다.

0개의 댓글