티스토리 뷰

(EffectiveJava) 규칙 1. 생성자 대신 정적 팩터리 메서드를 고려하라


정적 팩터리 메서드


  • 클래스를 통해 객체를 만드는 일반적인 방법은 public 으로 선언된 생성자 (constructor)를 이용하는 방법이다.

  • 그러나 모든 프로그래머가 반드시 알고 있어야 하는 방법이 하나 있다.

    • 클래스에 public으로 선언된 정적 팩터리 메서드(static factory method)를 추가하는 것
  • Boolean(Java의 기본 타입 중 하나인 boolean을 클래스화 한 것) 클래스에 대한 간단한 예제

      //boolean의 값을 Boolean 객체에 대한 참조로 변환
      public static Boolean valueOf(boolean b) {
              return b ? Boolean.TRUE : Boolean.FALSE;
      }
    • 클래스를 정의할 때 생성자 대신 정적 팩터리 메서드를 제공할 수 있음
    • 단, 정적 팩터리 메서드는 어떠한 디자인 패턴도 아님

정적 팩터리 메서드의 장점


1. 생성자와는 달리 정적 팩터리 메서드에는 이름(name)이 있다.

  • 정적 팩터리 메서드는 이름을 잘 짓기만 한다면 사용하기 쉽고 가동성도 높아진다.
    • 생성자에 전달되는 인자(parameter)들은 어떤 객체가 생성되는지를 설명하지 못함
    • 예) 소수일 가능성이 높은 BigInteger 객체를 생성하는 BigInteger(int, int, Random)은 BigInteger.probablePrime과 같은 이름의 정적 팩터리 메서드로 표현하면 더 이해하기 쉬움
  • 시그니처를 갖는 생성자를 여러개 정의할 필요가 있을 때 메서드 이름을 통해서 용도를 파악하기 쉽다.
    • 생성자에서는 인자의 순서를 바꾸는 방법으로 이를 해결하는데, 이 방법에는 아주 문제가 많음
      • API 사용자는 각각의 생성자 용도를 기억하지 못함
      • 즉, API 설명서를 참조하기 않고는 코드가 하는 일을 제대로 파악할 수 없음
    • 팩터리 메서드에는 이름이 있기 때문에 작명에 신경을 쓴다면 용도를 파악하기 쉬움

2. 생성자와는 달리 호출할 떄마다 새로운 객체를 생성할 필요가 없다.

  • 변경 불가능 클래스라면 이미 만들어둔 객체를 활용할 수 있다. (객체를 캐시(cache) 해놓고 재사용도 가능)
    • 위 소개한 Boolean.valueOf(Boolean) 메서드가 이 기법을 활용한 예
    • 동일한 객체가 요청되는 일이 잦고, 객체를 만드는 비용이 클 때 적용하면 성능을 크게 개선 가능
      • 경량(Flyweight) 패턴과도 유사함
  • 정적 팩터리 메서드를 사용하면 같은 객체를 반복해서 반환할 수 있어 어떤 시점에 어떤 객체가 얼마나 존재할지를 정밀하게 제어할 수 다.
    • 이런 기능을 가진 클래스는 개체 통제 클래스(instance-controlled class)라고 부름
    • 개체 통제 클래스를 작성하는 이유
      1. 개체수를 제어하면 싱글턴 패턴을 따르도록 할 수 있음
      2. 객체 생성이 불가능한 클래스를 만들 수도 있음
      3. 변경 불가능한 클래스의 경우 두개의 같은 객체가 존재하지 못하도록 할 수도 있음
        • a == b 일 때만 a.equals(b)가 참이 되도록 만들 수 있다는 것
        • 이렇게 구현된 클래스는 equals(Object) 대신 == 연산자를 사용할 수 있어 성능이 향상됨
          • enum이 이 기법을 사용함

3. 생성자와는 달리 반환값 자료형의 하위 자료형 객체를 반환할 수 있다.

  • 반환되는 객체의 클래스를 훨씬 유연하게 결정할 수 있다.
    • public으로 선언되지 않은 클래스의 객체를 반환하는 API를 만들 수 있음.
      • 구현 세부사항을 감출 수 있으므로 간결한 API가 가능
      • 인터페이스 기반 프레임워크 구현에 적합하며 인터페이스는 정적 팩터리 메서드의 반환값 자료형으로 이용됨
        • 관습상 Type이라는 인터페이스를 반환하는 팩터리 메서드는 Types라는 이름의 객체 생성 불가능 클래스 안에 둠
      • 예) Java 컬렉션 프레임워크에는 32개의 컬렉션 인터페이스 구현체가 들어 있는데, 변경이 불가능한 컬렉션과 동기화된 컬렉션 등이다.
        • 구현체들 거의 전부는 java.util.Collections라는 객체 생성 불가능 클래스의 정적 팩터리 메서드를 통해 이용하는데 실제 클래스는 public이 아님
        • 프레임워크 기법 덕에 간단해진 것은 단순히 API의 구조가 아리나 개념상의 무게(conceptual weight)임
          • 사용자들은 별도의 클래스 사용법 문서를 읽지 않고도 구현체를 이용
          • 이를 사용한 코드에서는 인터페이스만 보고 작성하기 때문에 일반적으로 바람적인 습관
    • JDK 1.5부터 도입된 java.util.EnumSet에는 public으로 선언된 생성자가 없으며, 정적 팩터리 메서드 뿐임
      • 메서드들은 enum 상수 개수에 따라 두 개 구현체 가운데 하나를 골라 클래스의 객체를 만들어 반환함
        • RegularEnumSet 객체를 반환하는데, 객체는 내부적으로 long 변수 하나만 사용함
        • enum 상수들이 64개보다 많은 경우에는 JumboEnumSet 객체를 반환하는데 내부적으로 long 형의 배열을 사용
      • 추후에 EnumSet 구현체들의 기능을 변경하거나 추가할 수도 있음
      • 클라이언트는 팩터리 메서드가 반환하는 객체의 실제 클래스를 알 수도 없고, 알 필요도 없음
        • 단지 EnumSet의 하위 클래스라는 사실만이 중요함
    서비스 제공자 프레임워크
    • 다양한 서비스 제공자들이 하나의 서비스를 구성하는 시스템
      • 정적 팩터리 메서드가 반환하는 클래스는 정적 팩터리 메서드가 정의된 클래스의 코드가 작성되는 순간에 존재하지 않아도 무방함
      • JDBD(Java Database Connectivity API)가 대표적 예이여 근간을 이루는 것이 팩터리 메서드임
    • 클라이언트가 세부 구현 내용을 몰라도 실제 구현된 서비스를 이용할 수있도록 함<
    • 서비스 제공자 프레임워크의 핵심 컴포넌트
      • 서비스 인터페이스
        • 서비스 제공자가 구현함
      • 제공자 등록 API
        • 구현체를 시스템에 등록하여 클라이언트가 쓸 수 있도록 함
      • 서비스 접근 API
        • 클라이언트에게 실제 서비스 구현체를 제공
        • 제공자 선택 기준을 제공할 수도 있고 그렇지 않으면 기본 구현체의 객체를 반환
      • 서비스 제공자 인터페이스 (옵션)
        • 서비스 제공자가 구현하는 것으로 서비스 구현체의 객체를 생성하기 위한 것
        • 없는 경우 구현체는 클래스 이름으로 등록되며 자바의 리플렉션 (reflection) 기능을 통해 객체로 만들어짐
      • 예) JDBC
        • Connection: 서비스 인터페이스
        • DriverManager.registerDriver: 제공자 등록 API
        • DriverManager.getConnection: 서비스 접근 API
        • Driver: 서비스 제공자 인테페이스
      • 서비스 제공자 프레임워크 패턴에는 다양한 변종들이 있음
        • 예) 서비스 접근 API에 어댑터 패턴을 적용하면 제공자에게 요구되는 것보다 풍부한 기능을 제공하는 서비스 인터페이스 객체를 제공하는 서비스 인터페이스 객체를 반환할 수 있음
            //서비스 제공자 인터페이스의 대략적인 모습
          
          //서비스 인터페이스
          public interface Service {
            ... //서비스의 고유한 메서드들이 이 자리에 온다
          }
          
          //서비스 제공자 인터페이스
          public interface Provider {
          
            Service newService();
          }
          
          //서비스 등록과 접근에 사용되는 객체 생성 불가능 클래스
          public class Services {
          
            private Services() {
            } //객체 생성 방지
          
            //서비스 이름과 서비스간 대응관계 보관
            private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
            public static final String DEFAULT_PROVIDER_NAME = "<def>";
          
            //제공자 등록 API
            public static void registerDefaultProvider(Provider p) {
              registerProvider(DEFAULT_PROVIDER_NAME, p);
            }
          
            public static void registerProvider(String name, Provider p) {
              providers.put(name, p);
            }
          
            //서비스 접근 API
            public static Service newInstance() {
              return newInstance(DEFAULT_PROVIDER_NAME);
            }
          
            public static Service newInstance(String name) {
              Provider p = providers.get(name);
              if (p == null) {
                throw new IllegalArgumentException("No provider registered with name: " + name);
              }
              return p.newService();
            }
          }

4. 형인자 자료형(parameterized type) 객체를 만들 때 편하다.

  • 아래와 같은 클래스의 생성자를 호출할 때는, 문맥상 형인자가 명백하더라도 반드시 인자를 형인자로 전달해야 한다.
    • 보통은 이렇게 형인자를 두 번 사용하게 됨
      • Java 1.6 까지는 생성자 호출 시에 자료형 유추를 지원하지 않음
        Map<String, List<String>> m = new HashMap<String, List<String>>();
    • 자료형 명세를 중복하면, 형인자가 늘어남에 따라 기록 복잡한 코드가 만들어짐
  • 정적 팩터리 메서드를 사용하면 컴파일러가 형인자를 스스로 알아내도록 할 수 있다.
    • 이런 기법은 자료형 유추(type interface)라고 함
    • 예) HashMap 클래스가 아래의 제네릭 정적 팩터리 메서드를 제공한다고 가정 해보면
        public static <K, V> HashMap<K, V> newInstance() {
                return new HashMap<K, V>();
        }
    • 위 메소드가 있으면 앞서 살펴본 선언물을 좀 더 간결하게 작성할 수 있음
      Map<String, List<String>> m = new HashMap<>();
      다행히도 자바 1.7부터는 생성자를 호출 할때도 자료형 유추를 사용할 수 있음
      • 따라서 표준 컬렉션 메서드에 정적 메[서드 팩터리 메서드를 추가할 필요가 없어짐

정적 팩터리 메서드의 단점


1. public이나 protected로 선언된 생성자가 없으므로 하위 클래스를 만들 수 없다.

  • 예를 들어, 자바의 컬렉션(Collections) 프레임워크에 포함된 기본 구현 클래스들의 하위 클래스는 만들수 없다.
    • 논쟁의 소지가 있긴 하지만 그래서 더 좋다는 사람도 있음
    • 그건 계승(inheritance) 대신 구성(composition) 기법을 쓰도록 장려한다는 이유 (규칙 16)

2. 정적 팩터리 메서드가 다른 정적 메서드와 확연히 구분되지 않는다.

  • API 문서를 보면 생성자는 다른 메서드와 뚜렷이 구분되지만, 정적 팩터리 메서드는 그렇지 않다.
    • 그렇기에 생성자 대신 정적 팩터리 메서드를 통해 객체를 만들어야 하는 클래스는 파악하기가 쉽지 않음
  • 따라서, 클래스나 인터페이스 주석을 통해 알리거나 정적 팩터리 메서드 이름을 지을 때 조심하는 수 밖에 없다.
    • 보통 쓰이는 정적 팩터리 메서드의 이름
      • valueOf(): 인자로 주어진 값과 같은 값을 갖는 객체를 반환한다는 뜻.
      • of(): valueOf를 더 간단하게 쓴 것. EnumSet덕분에(규칙32) 인기를 모은 이름.
      • getInstance(): 인자에 기술된 객체를 반환하지만, 인자와 같은 값은 값을 갖지 않는 수도 있음. 싱글턴 패턴을 따를 경우, 이 메서드는 인자 없이 항상 같은 객체를 반환함.
      • newInstance(): getInstance와 같지만, 호출할 때마다 다른 객체를 반환.
      • getType(): getInstance와 같지만, 반환될 객체의 클래스와 다른 클래스에 팩터리 메서드가 있을 때 사용. Type은 팩터리 메서드가 반활할 객체의 자료형임.
      • newType(): newInstance와 같지만, 반활될 객체의 클래스와 다른 클래스에 팩터리 메서드가 있을 때 사용. Type은 팩터리 메서드가 반활할 객체의 자료형임.

결론


  • 정적 팩터리 메서드와 public 생성자는 용도가 서로 다르면, 그 차이와 장단점을 이해하는 것이 중요하다.

  • 정적 팩터리 메서드가 효과적인 경우가 많으니, 정적 팩터리 메서드를 고려해 보지도 않고 무조건 public 생성자를 만드는 것은 삼가기 바람.


끝으로

이 글이 도움이 되었다면, Google 광고 한번씩 클릭 부탁 드립니다. 🙏🙏🙏

광고 클릭은 많은 힘이 됩니다!

반응형
댓글