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

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}
  • 디자인 패턴의 팩터리 메서드(Factory Method)와는 다름!
  • 해당 클래스의 인스턴스를 반환하는 단순한 정적 메서드

정적 팩터리 메서드가 생성자보다 좋은 점

  1. 이름을 가질 수 있다
  • 생성자: BigInteger(int, int, Random)에 비해
    정적 팩터리 메서드 BigInteger.probablePrime이 더 직관적이다.
  • 생성자 오버로딩을 이용하는 데에도 한계가 있다.
    • 하나의 시그니처로는 생성자를 하나만 가질 수 있다.
      • 매개변수의 순서를 다르게 하는 방식
        • Constructor(int i, String s)
        • Constructor(String s, int i)는 서로 다르다는 점을 이용하는 방식
        • 하지만 이 경우 직관성이 떨어지고, 혼란만 가중시킨다.
      • 반면, 정적 팩터리 메서드는 하나의 시그니처로 하나의 생성자만 가질 수 있는 제약을 받지 않는다.
  1. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
  • 인스턴스 통제(instance-controlled) 클래스
    • 인스턴스 미리 만들기, 캐싱 등으로 불필요한 객체 생성 회피 -> 성능 향상
    • 인스턴스 통제를 통해 객체를
      • 싱글턴(singleton)
      • 인스턴스화 불가(noninstantiable)
      • 불변 값 인스턴스에서 동치인 인스턴스가 하나임을 보장(Leibniz's law)
      • 플라이웨이트 패턴
      • 열거형(인스턴스가 하나임을 보장)
        만들 수 있다.
  1. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
  • 반환 객체의 클래스를 선택 가능한 유연성

    • API를 작게 유지
    • 인터페이스 기반 프레임워크: 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용
      • 구현 객체를 인터페이스로 다룬다는 점에서 API 학습 난이도를 낮추며, 다형성을 활용하기 좋다.
  • Java8이전의 경우 인터페이스에서 정적 메서드 반환 불가

    • 동반 클래스 사용해야:
    public interface Type {
        ...
    }

    이를 반환하는 정적 메서드typeFactory() Type 내에 생성이 불가능하기에

    public class Types {
        static Type typeFactory() {
        ...
        }
    }

    위와 같은 방식으로 동반 클래스(companion class)를 만들어야 했다.

    • 예시) java.util.Collections
  • 자바8 이후로는 인터페이스가 정적 메서드를 가질 수 있다.

    • 즉 동반 클래스를 가질 필요가 적다.
    • 하지만 인터페이스는 public 정적 멤버만 허용하며
    • 자바9에서 private 정적 메서드까지 허용하지만, 어쨌든 정적 필드와 정적 멤버 클래스는 여진히 public만 가능하다.
    • 이런 이유로 package-private 클래스에 정적 메서드 구현 코드 일부를 둬야 하는 경우도 있다.
  1. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  • 유연하게 반환 하위 클래스를 선택, 변경할 수 있다.
  • 위와 마찬가지로, 다형성을 활용하는 데 용이하다. 클라이언트는 실제 어느 하위 클래스의 인스턴스인지를 알 필요가 없다.
  • 예시: java.util.EnumSet.java의 경우
/**
 * Creates an empty enum set with the specified element type.
 *
 * @param <E> The class of the elements in the set
 * @param elementType the class object of the element type for this enum
 *     set
 * @return An empty enum set of the specified type.
 * @throws NullPointerException if {@code elementType} is null
 */
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);
}

원소의 수에 따라 RegularEnumSet이나 JumboEnumSet을 반환한다.

  • 하위 클래스이기만 한다면

    • 클라이언트가 하위 클래스의 존재를 모르며,

    • 필요 없는 하위 클래스는 삭제해도 문제가 없으며

    • 필요한 다른 클래스를 추가해도 된다는 점에 주목하라.

  1. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
  • 서비스 제공자 프레임워크(service provider framework)

  • 대표적인 예로 JDBC(Java Database Connectivity)

  • 제공자(provider)인 서비스 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 담당 -> 클라이언트를 구현체로부터 분리!

  • 구성: 3개의 핵심 컴포넌트(경우에 따라 +1)
    서비스 인터페이스(service inteface): 구현체의 동작을 정의

  • 제공자 등록 API(provider registration API): 제공자가 구현체를 등록할 때 사용

  • 서비스 접근 API(service access API): 클라이언트가 서비스의 인스턴스를 얻을 때 사용
    클라이언트가 원하는 구현체의 조건을 명시하여 반환받음, 명시하지 않으면 기본 구현체 혹은 지원 구현체 중 하나를 반환

    • 유연한 정적 팩터리
		```java
		    @CallerSensitive
		    public static Connection getConnection(String url,
		        java.util.Properties info) throws SQLException {
		
		        return (getConnection(url, info, Reflection.getCallerClass()));
		    }
		...
		    @CallerSensitive
		    public static Connection getConnection(String url,
		        String user, String password) throws SQLException {
		        java.util.Properties info = new java.util.Properties();
		
		        if (user != null) {
		            info.put("user", user);
		        }
		        if (password != null) {
		            info.put("password", password);
		        }
		
		        return (getConnection(url, info, Reflection.getCallerClass()));
		    }
		...
		    @CallerSensitive
		    public static Connection getConnection(String url)
		        throws SQLException {
		
		        java.util.Properties info = new java.util.Properties();
		        return (getConnection(url, info, Reflection.getCallerClass()));
		    }
		...
		        //  Worker method called by the public getConnection() methods.
		    private static Connection getConnection(
		        String url, java.util.Properties info, Class<?> caller) throws SQLException {
		        /*
		         * When callerCl is null, we should check the application's
		         * (which is invoking this class indirectly)
		         * classloader, so that the JDBC driver class outside rt.jar
		         * can be loaded from here.
		         */
		        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
		        if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
		            callerCL = Thread.currentThread().getContextClassLoader();
		        }
		
		        if (url == null) {
		            throw new SQLException("The url cannot be null", "08001");
		        }
		
		        println("DriverManager.getConnection(\"" + url + "\")");
		
		        ensureDriversInitialized();
		
		        // Walk through the loaded registeredDrivers attempting to make a connection.
		        // Remember the first exception that gets raised so we can reraise it.
		        SQLException reason = null;
		
		        for (DriverInfo aDriver : registeredDrivers) {
		            // If the caller does not have permission to load the driver then
		            // skip it.
		            if (isDriverAllowed(aDriver.driver, callerCL)) {
		                try {
		                    println("    trying " + aDriver.driver.getClass().getName());
		                    Connection con = aDriver.driver.connect(url, info);
		                    if (con != null) {
		                        // Success!
		                        println("getConnection returning " + aDriver.driver.getClass().getName());
		                        return (con);
		                    }
		                } catch (SQLException ex) {
		                    if (reason == null) {
		                        reason = ex;
		                    }
		                }
		
		            } else {
		                println("    skipping: " + aDriver.driver.getClass().getName());
		            }
		
		        }
		
		        // if we got here nobody could connect.
		        if (reason != null)    {
		            println("getConnection failed: " + reason);
		            throw reason;
		        }
		
		        println("getConnection: no suitable driver found for "+ url);
		        throw new SQLException("No suitable driver found for "+ url, "08001");
		    }

​ 제공된 파라미터에 따라서 실제로 getConnection() 작업을 하는 메서드에 형식을 맞춰 파라미터를 넘기고, 인터페이스로서 Driver를 받고 있다.

  • 그런데 왜 리플렉션을 사용할까?

  • 서비스 제공자 인터페이스(service provider interface): 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명

    • 리플렉션?

      • 서비스 제공자 프레임워크 패턴의 변형

      • 브리지 패턴: 서비스 접근 API가 공급자가 제공하는 것보다 더 풍부한 서비스 인터페이스를 클라이언트에 반환

      • DI 프레임워크

        • java,util.ServiceLoader: 자바6부터 제공되는 범용 서비스 제공자 프레임워크
        • JDBC의 경우 자바6 이전에 등장했기에 위를 사용 안 함.

단점

  1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    • 상속보다 컴포지션을 사용하도록 유도하고
    • 불변 타입으로 만들기 위한 제약이 된다는 점에서 장점이 되기도 한다.
  2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
    • 생성자처럼 API 설명에 명확히 들어나지 않기 때문(JavaDocs)
    • API 문서를 잘 작성하고 메서드명을 규약에 따라 짓는 것으로 문제를 완화해야:
    • 정적 팩터리 메서드에 흔한 명명 방식
      • from: 매개변수를 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
        • Date d = Date.from(instant);
      • of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
        • Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING)
      • valueOf: from과 of의 더 자세한 버전
        • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
      • instance 혹은 getInstance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
        • StackWalker luke = StackWalker.getInstance(options);
      • create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
        • Object newArray = Array.newInstance(classObject, arrayLen);
      • getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 매서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 객체의 타입이다.
        • FileStore fs = Files.getFileStore(path);
      • newType: getType과 같으나, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
        • BufferedReader br = Files.newBufferedReader(path);
      • type: getType과 newType의 간결한 버전
        • List<Complaint> litany = Collections.list(legacyLitany);

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

좋은 웹페이지 즐겨찾기