책/Effective Java 3E

[2장 객체 생성과 파괴] 아이템1. 생성자 대신 정적 펙터리 메서드를 고려하라.

경딩 2024. 9. 23. 19:30

 

정적 팩터리 메소드 : 생성자와 별도로 그 클래스의 인스턴스를 반환하는 단순한 정적메서드.

    public static void main(String[] args) {
       Boolean bool1 = new Boolean(true); // new 생성자로 인스턴스 생성
        //  public Boolean(boolean value) {
        //        this.value = value;
        //    }
       Boolean bool2 = Boolean.valueOf(true); // 정적 팩터리 메소드를 활용한 인스턴스 생성
        //   public static Boolean valueOf(boolean b) {
        //        return (b ? TRUE : FALSE);
        //    }
    }

 

정적팩터리와 생성자를 비교하여 장단점을 알아보자

 

장점

 

장점1. 이름을 가질 수 있다

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

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

   값이 소수인 BigInteger 를 반환하다는 의미를 더 잘 설명하는 쪽은?
   BigInteger b1 = new BigInteger(int, int, Random); // 생성자로 인스턴스 생성
   BigInteger b2 = BigInteger.probablePrime(); // 정적 팩토리 매서드로 인스턴스 생성

 

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

불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로  불필요한 객체 생성을 피할 수 있다.  플라이웨이트 패턴도 이와 비슷한 기법이라 할 수 있다.

 

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

반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성으로 API  구현 클래스는 공개하지않고도 그 객체를 반환할 수 있어 API  를 작게 유지할 수 있다. 이는 인터페이스를 정적 팩터리 메서드의 반환타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이다.

 

반환하는 타입을 인터페이스 타입으로 할 경우, 해당 인터페이스를 구현하는 다름 타입의 객체들을 반환 할수 있음을 말한다(상혹). ex ) java.util.Colletions

 

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

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

가령 EnumSet 클래스는 publi 생성자 없이 오직 정적 팩터리 메서드만 제공한다.
만약 원소가 64개 이하이면 원소들은 long 변수 하나로 관리하는
RegularEnumSet의 인스턴스를,
65개 이상이면 long 배열로 관리하는 JunboEnumSet의 인스턴스를 반환한다.
클라이언트는 이 두 클래스의 존재를 몰라도 된다.

 

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

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

이런 유연함을 서비스 제공자 프레임워크를 만드는 근간이 된다. 대표적인  서비스 제공자 프레임워크로는 JDBC(Java Database Connectivity) 가 있다. 서비스 제공자 프레임워크에서의 제공자는 서비스의 구현체다. 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

서비스 제공자 프레임 워크는 3개의 핵심 컴포넌트로 구성되어 있다.

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

 

JDBC를 예시로 보면 위의 컴포넌트에 대하여 아래와 같은 요소들이 역할을 수행한다.

  • 서비스 인터페이스 : Connection
  • 제공자 등록 API : DriverManager.registerDriver
  • 서비스 접근 API : DriverManager.getConnection


이 중 서비스 접근 API는 조건을 명시해서 사용하거나 조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 돌아가며 반환한다. 이 부분이 유연한 정적 팩토리라 볼 수 있다.

JDBC에서 DriverManager.registerDriver()메서드를 통해 Driver를 등록한다.

그 후 DriverManager.getConnection()메서드를 통해 DB 서버의 url, 사용자 이름과 비밀번호를 넘겨줘서 연결을 진행하는데, 이 정적 메서드는 내부의 private 메서드로 작업을 위임한다.

위의 private getConnection()메서드에서 registerDriver()메서드를 통해 등록된 드라이버들을 순회하면서 적절한 드라이버를 통해 Connection을 반환한다.

 getConnection()메서드가 작성된 시점에는 Driver가 존재하지 않았지만, 실행 시점에서 등록된 Driver를 통해 새로운 인스턴스를 할당받는 구조로 JDBC는 동작한다.

 

단점

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

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

 

상속과 컴포지션 차이 : https://velog.io/@vino661/%EC%83%81%EC%86%8D%EA%B3%BC-%EC%BB%B4%ED%8F%AC%EC%A7%80%EC%85%98%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C

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

 

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

 

핵심정리
정적 팩터리 메서드와  public 생성자는 각자의 쓰임새가 있으니 상대적인 장담점을 이해하고 사용하는 것이 좋다.
그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

 

 

참고 블로그: https://lxxjn0-dev.netlify.app/effective-java-item-01
참고도서: Effective Java 3/E