콘텐츠로 이동

2022 05 19

2022-05-19

Effective Java Item 1. 생성자 대신 정적 팩토리 메서드를 고려하라

  • 개요
    • 클라이언트는 그동안 public 생성자를 통해 인스턴스를 획득하곤 했음
    • 정적 팩토리 메서드를 클래스가 제공해서, 클래스의 인스턴스를 반환하는 정적 메서드를 만들어보자
    • 정적 팩토리 메서드 != 팩토리 메서드 패턴
      • 디자인 패턴이랑 정적 팩터리 메서드 노상관
    • 생성자 대신 정적 팩토리 메서드 제공하는 것은 장단점이 있음
  • 장점 1. 이름이 생긴다
    • 코드의 가독성을 높여줌
    • 각 생성자는 별도의 시그니처를 가지는데, 이 제약에서 벗어나고자 눈가림하듯 순서만 바꾸면 쓰레기 코드
      • 여러 시그니처를 갖는 생성자가 필요하다면 정적 팩터리 메서드 써보자
  • 장점 2. 생성자와 달리 매번 새로운 객체를 만들지 않아도 된다
    • 생성자는 리턴값을 가질 수 없음. 따라서 발생하는 몇가지 불편함이 있음
    • 불변 객체라면 그거 쓰면되고,
    • 미리 만들어둔것 캐싱해두고 쓰면 될 듯!
    • 객체를 만드는 비용이 크다면 성능 크게 개선 가능
    • 어떤 시점에 어떤 객체가 얼마나 존재하는지를 정밀하게 제어할 수 있다는 것이 장점 => 생성자에겐 없던 객체 통제 능력 (instance-controlled class)
      • Singleton 만들기 쉬워짐
      • 객체 생성 불가능하게 만들수도 있음
      • 불변 객체 제공도 가능
  • 장점 3. 하위 자료형을 반환할 수 있다
    • 반환되는 객체의 클래스를 훨씬 유연하게 결정 가능
      • 기존 생성자가 클래스의 관리자로써 못하던 일을 가능케 해줌
    • 구현의 세부사항을 감출 수 있는 좋은 코드 작성 가능
      • interface-based-framework에서 좋음
      • Java 8 이전에는 인터페이스에 정적 메서드 X
        • JCF에서 45개의 utility implementation of interfaces가 있었는데 공통적으로 다음을 제공
          • unmodifiable
          • synchronized
  • 장점 4. input 파라미터에 따라 다른 인스턴스를 반환할 수 있다
    • ex. EnumSet
      • long => RegularEnumSet
      • long array => JumboEnumSet
    • 이렇게 하면 추후에 EnumSet을 상속받은 구현체가 생겼을 때 손쉽게 붙일 수 있겠지?
      • 유지/보수/확장에 유리한 코드 작성 가능
  • 장점 5. 반환하고자 하는 클래스가 런타임에 생겨도 괜찮음
    • 정적 팩터리 메서드가 반환하는 객체의 클래스는 정적 팩터리 메서드가 정의된 클래스의 코드가 작성된 순간에 없어도 OK
    • 서비스 제공자 프레임워크 (ex. JDBC)
      • 다양한 서비스 제공자들이 하나의 서비스를 구성하는 시스템
      • 클라이언트는 실제 구현된 서비스를 이용
      • 핵심 컴포넌트
        1. 서비스 인터페이스 (Connection)
          • 서비스 제공자가 구현
        2. 제공자 등록 API (DriverManager.registerDriver)
          • 구현체를 시스템에 등록하여 클라이언트가 쓸 수 있도록
        3. 서비스 접근 API (DriverManager.getConnection)
          • 클라이언트에게 실제 서비스 구현체 제공
        4. 서비스 제공자 인터페이스 (Driver)
          • 서비스 제공자가 구현하며, 서비스 구현체의 객체를 생성하기 위함
          • 구현체는 클래스 이름으로 등록되며, 리플렉션을 통해 만들어짐
      • 서비스 접근은 결국 유연한 정적 팩토리 메서드
        // 서비스 인터페이스
        public interface Service {}
        
        // 서비스 제공자 인터페이스
        public interface Provider {
            Service newService();
        }
        
        // 서비스 등록과 접근에 사용되는 객체 생성 불가능 클래스
        public class Services {
          private Services() {
          }
        
          // 서비스 이름과 서비스 간 대응관계 보관
          public static final String DEFAULT_PROVIDER_NAME = "<def>";
          private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
        
          // 제공자 등록 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 (Objects.isNull(p)) {
                  throw new IllegalArgumentException();
              }
              return p.newService();
          }
        }
        
  • 단점 1. 정적 팩터리 메서드만 있는 클래스라면, public/protected로 선언된 생성자가 없기에 하위 클래스 못 만듦
    • 오히려 좋은 부분 : 상속보다 컴포지션 유도
  • 단점 2. 다른 정팩메와 완전한 구분이 어려움

    • 따라서 정팩메 이름 지을 때 잘 지어라
    • valueOf: 주어진 값과 같은 값을 갖는 객체 반환, 형변환 메서드
    • of: valueOf의 축약
    • getInstance: 싱글턴이라면, 항상 같은 객체 반환
    • newInstance: getInstance와 같지만, 호출될 때 마다 다른 인스턴스
    • getType: getInstance와 같지만, Type 형을 반환해야하는 경우
    • newType: newInstance와 같지만, Type 형을 반환해야하는 경우
    • 내 생각

      • 결국 Java의 생성자가 충분히 강력하지 않아서 발생하는 문제를 해결하고자 정팩메 사용
        • 생성자는 클래스의 생명주기를 충분히 관리하고, 완전한 중앙 통제형 집권자여야함.
          • 어떤 시기에 어떤 객체가 있고, 몇 개가 있고 등등...
      • 난 정팩메를 언제 많이 쓰지? 나는 정팩메를 똑바로 쓰고 있는가?
        • 그냥 생성자마냥 쓰는거 아닌가? 빌더 패턴 마냥 쓰는거 아닌가? 그냥 필드 추출해서 DTO로 감쌀때만 쓰지 않는가?
        • Enum에서 조건에 맞는 인스턴스를 반환해주는 경우
        • Enum에 등록된 반환 객체도 singleton
        • singleton를 반환하는 정적 팩터리 메서드가 좋긴하겠다...!
          public static FilterStrategyFactory of(String mainnet, String category, String ventureCapital) {
              return Arrays.stream(values())
                      .filter(strategy -> strategy.isMainnetEmpty == mainnet.isEmpty())
                      .filter(strategy -> strategy.isCategoryEmpty == category.isEmpty())
                      .filter(strategy -> strategy.isVentureCaptialEmpty == ventureCapital.isEmpty())
                      .findFirst()
                      .orElseThrow(() -> new IllegalStateException("발생할 수 없는 로직입니다."));
          }
          
      • 도메인 받아서 필드 추출해서 쓱 DTO 만들때도 많이 씀
        • https://github.com/CryptoHows/cryptohows-web-backend/blob/master/src/main/java/xyz/cryptohows/backend/project/ui/dto/CoinResponse.java
        • 새로운 객체를 그냥 만드는 것에 대한 책임을 넘겨주는 느낌
          • 사실 꼭 정팩메일 필요가 없어보이긴 함
          • 그냥 무지성으로 쓰고 있긴했음
          • 다만 좋은 것은, of/toList 등으로 이름을 붙여줄 수 있다는 점! (생성자였으면 이 만큼 직관적이진 않았을 수도)
            @Getter
            public class CoinResponse {
            
                private final Long id;
                private final String coinSymbol;
                private final String coinInformation;
            
                public CoinResponse(Long id, String coinSymbol, String coinInformation) {
                    this.id = id;
                    this.coinSymbol = coinSymbol;
                    this.coinInformation = coinInformation;
                }
            
                public static CoinResponse of(Coin coin) {
                    return new CoinResponse(
                            coin.getId(),
                            coin.getCoinSymbol(),
                            coin.getCoinInformation()
                    );
                }
            
                public static List<CoinResponse> toList(List<Coin> coins) {
                    return coins.stream()
                            .map(CoinResponse::of)
                            .collect(Collectors.toList());
                }
            }
            
    • Question
      • 정적 팩터리 메서드 vs 빌더 패턴
      • 싱글턴의 단점?