본문 바로가기

JAVA/Effective Java

Item 1) 생성자 대신 정적 팩토리 메소드를 고려하라

728x90

1. 정적 팩토리 메소드란 ? 

 

정적 팩토리 메소드(Static Factory Method)란 보다 명시적으로 객체를 생성하는 클래스 메소드다.

대표적으로 java.time.LocalTime에서 이 것을 찾을 수 있다.

    //-----------------------------------------------------------------------
    /**
     * Obtains an instance of {@code LocalTime} from an hour and minute.
     * <p>
     * This returns a {@code LocalTime} with the specified hour and minute.
     * The second and nanosecond fields will be set to zero.
     *
     * @param hour  the hour-of-day to represent, from 0 to 23
     * @param minute  the minute-of-hour to represent, from 0 to 59
     * @return the local time, not null
     * @throws DateTimeException if the value of any field is out of range
     */
    public static LocalTime of(int hour, int minute) {
        HOUR_OF_DAY.checkValidValue(hour);
        if (minute == 0) {
            return HOURS[hour];  // for performance
        }
        MINUTE_OF_HOUR.checkValidValue(minute);
        return new LocalTime(hour, minute, 0, 0);
    }

/////////////////////////////////-----------------------------------------------


/**
     * Constructor, previously validated.
     *
     * @param hour  the hour-of-day to represent, validated from 0 to 23
     * @param minute  the minute-of-hour to represent, validated from 0 to 59
     * @param second  the second-of-minute to represent, validated from 0 to 59
     * @param nanoOfSecond  the nano-of-second to represent, validated from 0 to 999,999,999
     */
    private LocalTime(int hour, int minute, int second, int nanoOfSecond) {
        this.hour = (byte) hour;
        this.minute = (byte) minute;
        this.second = (byte) second;
        this.nano = nanoOfSecond;
    }

 

 

위 코드처럼 LocalTime 클래스는 생성자를 private으로 선언하여 외부 클래스로의 생성자 호출을 막아놨다.

그리고 of 라는 정적 팩토리 메소드에서 생성자를 호출하는 방식으로 구현되있는 걸 볼 수 있다.

이와 같이 생성자의 역할을 대신해주는 메소드를 정적 팩토리 메소드라고 한다.

 

2. 생성자와의 차이점

그렇다면 이 정적 팩토리 메소드가 생성자와의 차이점에 대해서 알아보겠다.

1) 이름을 가질 수 있다.

   생성자의 시그니처(메소드 이름과 매개변수 리스트의 조합)만으로는 내가 만드려는 객체를 표현하기 어렵다.(특히 매개변수가 많아지고, 같은 타입의 매개변수가 많아진다면) 이런 생성자 패턴은 다른 개발자가 실수를 하기 쉬운 패턴이기도 하고, 클래스 설명문서를 꼼꼼히 읽어보지 않고서는 이해하기 힘든 패턴이다.

  반면, 정적 팩터리 메소드는 메소드 이름을 지정할 수 있다는 장점을 가지고 있기 때문에 객체를 표현하기 쉬울뿐만 아

니라 같은 시그니처를 가진 생성자라도 메소드 이름으로 구분지을수 있다는 장점을 가지고 있다.

 

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

  자주 사용되는 불변(immutable) 객체가 정해져 있다면 인스턴스를 미리 만들어놓고 캐싱할 수 있는 구조로 만들 수 있다. 특히 생성비용이 큰 객체가 자주 요청되는 경우 더욱 큰 이득을 볼 수 있다.

 

/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }
    
    
    
    
    //..............
    
    
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

   위 코드는 java.lang.Integer의 코드 중 일부다. 정적 팩토리 메소드인 valueOf를 보면 값이 IntegerCache.low와 high 사이에 있으면  캐시값을 리턴하는 걸 볼 수 있다. (JVM default low, high값은 -128,127)

 

 

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

 

  생성자는 리턴 값이 없는 반면 정적 팩토레 메소드를 리턴값을 지정할수 있다. 그러므로 리턴값을 하위 객체로 지정할 수 있다. 그리하여 반환할 객체를 자유롭게 선택하여 유연성을 가질 수 있다. (Java 8 버전부터 인터페이스에서도 정적 메소드를 선언할 수 있다. )

 

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

  위의 3번 내용과 비슷하다. 들어오는 매개변수에 따라 다른 객체를 반환하는 것이다. 사용하는 프로그래머 입장에서는 서로 다른 구현체를 모르더라도 인터페이스만 알고 있다면 사용하는데 아무런 문제가 없다.

 

    /**
     * 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 <tt>elementType</tt> 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);
    }

  EnumSet 은 public 생성자 없이 noneOf와 같은 정적 팩토리 메소드만을 제공하는데 universe(원소 수)가 64개 이하면 RegularEnumSet 64 보다 많으면 JumboEnumSet을 제공한다. RegularEnumset은 long 변수 하나를 bit flag 처럼 사용한다고 한다. 그러므로 long이 64비트이므로 원소를 64개까지만 사용할수 있고 bit manipultation 이라 조금 더 빠르다고 한다. 반면 JumboEnumSet은 long [] 배열을 사용하므로 제한 없이 사용할 수 있다.

  이러한 정적 팩토리 메소드의 장점은 나중에 RegularEnumSet가 없어지거나 다른 구현체를 사용하더라도 사용자가 똑같은 정적 팩토리 메소드를 사용한다는 장점이다. (BlackBox 원칙)

 

 

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

  프로그래머를 구현체로부터 독립시켜준다. 이러한 특징은 서비스 제공자 프레임워크(Service Provider Framework)에서 자주 나타난다.

 

+ Service Provider Framerwork 구성요소

1) Service Interface : 구현체의 동작을 정의하는 인터페이스

2) Service Registration API :  서비스 제공자가 구현체를 등록하는 API

3) Service Access API : Service Interface의 구현체를 얻어오는 API (정적 팩토리 메소드)

4) Service Provider Interface : Service Interface의 하위 객체를 생성해주는 API가 정의된 Interface


  자바에서 DBMS에 접근할때 사용되는 JDBC 에서 Connection이 인터페이스, DriverManger.registerDriver가 서비스 등록 API, DriverManger.getConnection이 서비스 접근 API 역활, Driver가 서비스 제공자 인터페이스 역할을 한다. (즉 JDBC에서 Connection이 동작을 정의하고 각 DBMS사에서 만든 구현체를 registerDriver로 등록하고 getConnection이라는 정적 팩토리 메소드를 사용한다.)

 

6) 단점

 

  정적 팩토리 메소드는 생성자와 다르게 규칙이 없기 때문에 프로그래머가 찾기 힘들다는 단점이 있다. 그러므로 정적 팩토리 메소드를 만들 때 적절한 명명 방식을 사용하여 예측가능하게 만드는 것이 중요하다. 아래는 정적 팩토리 메소드에서 자주 사용되는 명명 방식들이다.

 

  • from : 하나의 매개변수로 부터 생성 
  • of : 여러개의 매개변수를 받아서 객체 생성
  • getInstace, instance : 인스턴스를 생성 (이전에 생성했던 것과 같을수도 있음)
  • newInstance, create : 새로운 인스턴스 생성(매번 새로운 것을 생성함)
  • getType, newType : 위의 get, new 와 같지만 생성할 클래스가 아닌 다른 클래스에 정적 팩토리 메소드를 정의할 때 사용한다.
	Instant instant = Instant.now();//현재 시스템 시각
        Date date  = Date.from(instant);

        LocalTime localTime = LocalTime.of(10,24);
        
        Connection connection = DriverManager.getConnection("ssafy","ssafy","ssafy");

        Integer [] arr = (Integer[]) Array.newInstance(Integer.class, 10);
        arr[0] = 1;