본문 바로가기

Reading/Effective Java

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

정적 팩터리 메서드의 장점 5가지


1. 이름을 가질 수 있다.

 

정적 팩터리 메서드는 생성자와 달리 반환된 객체의 특성과 역할을 묘사하기 쉽고, 시그니처가 같은 생성자가 여러개 필요하면 이름으로 구분지을 수 있다. 

 

public class Refrigerator{
    String name;
    String manufacturer;
    
    public Refrigerator(String name) {
        this.name = name;
    }
    
//    같은 타입을 파라미터로 받는 생성자가 1개 이상일 수 없다.
//    public Refrigerator(String manufacturer) {
//        this.manufacturer = manufacturer;
//    }

    //public static 메소드가 더 읽기 편하고 이해하기 쉽다.
    public static Refrigerator withName(String name){
        return new Refrigerator(name);
    }
    
    public static Refrigerator withManufacturer(String manufacturer){
        Refrigerator refrigerator = new Refrigerator();
        refrigerator.manufacturer = manufacturer;
        return refrigerator;
    }
    
    public static void main(String[] args) {
        Refrigerator refrigerator1 = new Refrigerator("special 4 Door");
        Refrigerator refrigerator2 = Refrigerator.withName("special 4 Door");
        Refrigerator refrigerator3 = Refrigerator.withManufacturer("대기업");
    }
}

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

 

public class Refrigerator{

    public Refrigerator() { }
    
    /** 인스턴스를 새로 생성하지 않아도 된다. **/
    private static final Refrigerator GOOD_TEMPERATURE = new Refrigerator();
    
    public static Refrigerator getRefrigerator(){
        return GOOD_TEMPERATURE;
    }

    public static void main(String[] args) {
	Refrigerator refrigerator = Refrigerator.getRefrigerator();
    }
}

 

불변 클래스(immutable class): 변하지 않는 클래스는 인스턴스를 미리 만들거나 새로 생성한 인스턴스를 캐싱해 재활용 하는 식으로 불필요한 객체 생성을 피한다. 따라서 생성 비용이 큰 같은 객체가 자주 요청되는 상황에서 성능을 끌어올려준다.

이와 유사한 예로 Boolean.valueOf(boolean b) 가 있다. 

 

{
  ...
  public static final Boolean TRUE = new Boolean(true);
  public static final Boolean FALSE = new Boolean(false);
  ...
  @HotSpotIntrinsicCandidate
  public static Boolean valueOf(boolean b) {
      return (b ? TRUE : FALSE);
  }
  ...
}

 

플라이웨이트 패턴(flyweight pattern): 공통된 객체를 서로 공유하여 사용해서 메모리 사용량을 최소화하는 디자인 패턴으로 유사하다.

--> 싱글톤 패턴과 유사해 보인다. 어떤 차이가 있는지 나중에 정리하자

 

인스턴스 통제(instance controlled) 클래스: 반복되는 요청에 같은 객체를 반환하여, 정적 팩터리 방식의 클래스는 어느 인스턴스를 살아있게 할지 철저하게 통제가 가능하다.

 

인스턴스를 통제하면

(1) 싱글턴으로 만들수있다  / (2) 인스턴스화 불가로 만들수있다. / (3) 불변 값 클래스에서 동치인 인스턴스가 단 하나임을 보장할 수 있다.

그리고 인스턴스 통제는 플라이웨이트 패턴의 근간이되고, 열거 타입은 인스턴스가 하나만 만들어짐을 보장한다.


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

 

반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성을 제공한다.

 

/** 인터페이스 **/
public interface RefrigeratorInterface {
    public static Refrigerator getRefrigerator(){
        return new Refrigerator.MiniRefrigerator();
    }
}

public class Refrigerator{
  /** 하위 타입 **/
  static class MiniRefrigerator extends Refrigerator{ }

  public static void main(String[] args) {
      Refrigerator refrigerator = RefrigeratorInterface.getRefrigerator();
  }
}

 

인터페이스 기반 프레임워크(아이템20)를 만드는 핵심 기술이며, 외부 클라이언트는 실제 내부 구현체를 몰라도 인터페이스만 가지고 코딩이 가능하다. Collections가 그 예다. 구현체를 직접 만들 수 없고, Collections의 정적 팩터리 메서드를 통해서만 구현체를 만들 수 있다.

public 공개 부담이 줄어들고, 클라이언트가 알아야하는 API 사용에서의 개념의 개수와 난이도도 줄었다.

팩토리를 사용하는 코드가 구현체가 아닌 인터페이스 타입으로 코딩하는건 좋은일이다. 

 

이건 내가 헷갈렸던건데,

자바8부터 private static을 사용할 수 있다. 처음엔 private static을 왜 사용하지 싶었다. static 메서드는 static 메소드만 호출할 수 있다. public으로된 정적 팩터리 메서드가 여러개 있고, 그 중에서 공통된 게 있다면 private static으로 빼야하기 때문. 공통 부분은 public으로 공개할 이유가 없으니까! 프로젝트 진행하면서 dto에서 자주 썼었는데 순간 까먹었다..


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

 

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

 

  /** 하위 타입 **/
  static class MiniRefrigerator extends Refrigerator{ }

  /** 매개변수에 따른 다른클래스 호출(상속) **/
  public static Refrigerator getRefrigerator(boolean flag){
      return flag?new Refrigerator() : new MiniRefrigerator();
  }

  public static void main(String[] args) {
      Refrigerator miniRefrigerator = Refrigerator.getRefrigerator(false);
      Refrigerator refrigerator = Refrigerator.getRefrigerator(true);
  }

 

EnumSet 클래스는 public 생성자 없이 오직 정적 팩터리만 제공하고, 원소의 수에 따라 두가지 하위 클래스 중 하나를 반환한다.

 

 

Jumbo와 Regular는 클라이언트 입장에서 알 필요도 없고, 미래에 내부 구현체가 바뀔 때 유연성을 가진다. EnumSet의 하위 클래스기만 하면 되니까.


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

 

서비스 제공자 프레임워크(service provider framework)의 근간이 되고, 대표적으로 JDBC(Java Database Connectivity)가 있다. 제공자(provider)는 서비스의 구현체고, 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리한다. 컴포넌트는 아래 4가지가 존재한다.

 

  • [ JDBC의 Connection ] 서비스 인터페이스 (service interface): 구현체의 동작을 정의
  • [ JDBC의 DriverManager.registerDriver() ] 제공자 등록 API (provider registration API): 제공자가 구현체를 등록할 때 사용 
  • [ JDBC의 DriverManager.getConnection() ] 서비스 접근 API (service access API): 클라이언트가 서비스의 인스턴스를 얻을 때 사용, 원하는 구현체의 조건을 명시할 수 있고, 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환한다. 
  • [JDBC의 Driver] 서비스 제공자 인터페이스 (service provider interface): 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명, 만약 없다면 각 구현체를 인스턴스로 만들 때 리플렉션을 사용해야한다. 

 

코드로 간략하게 설명하자면

 

public class Jdbc {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        // Class.forName("com.mysql.cj.jdbc.Driver");
        Driver driver = new Driver();
        DriverManager.registerDriver(driver);
        
        Connection connection = 
                DriverManager.getConnection("url", "root", "root");
    }
}

 

 

com.mysql.cj.jdbc.Driver는 아래와 같이 java.sql.Driver(서비스 인터페이스)의 구현체다. 그리고 제공자 등록 API인 DriverManager.registerDriver()를 이용해서 구현체를 등록할 수 있다. Class.forName("")을 이용하면 구현체 등록과정을

거치지않고 사용할 수 있는데, 그게 가능한 이유가 아래 코드처럼 정적 블록이 있어서 클래스를 로드할 때 registerDriver() 과정을

수행하기 때문이다. Reflection.getCallerClass는 이 메서드를 호출한 클래스를 반환하는 것이다. 

 

 

 

registerDriver()의 과정을 좀 더 살펴보면 static 필드인 registerDrivers에 현재 드라이버를 저장하는 것을 볼 수 있다.

CopyOnWriteArrayList가 어떤 List인지 모르지만, addIfAbsent를 통해 없다면 추가하는 것으로 볼 수 있다.

즉 정적 팩터리 메서드를 작성하는 시점에서 반환할 객체의 클래스가 존재하지 않고, void로 처리하고있다. static 필드에 올리면서!

 

 

 

다음으로 getConnection 과정을 살펴보면, user와 password를 Properties에 담고 private static getConnection을 호출한다.

 

 

아까 Driver를 등록하는 과정에서 void로 처리했던 것을 ClassLoader 시점에서 driver를 주입받는다.

가지고 있는 registerDrivers를 forEach로 순회하면서 isDriverAllowed를 통해서 현재 registerDriver와 이 메서드를 호출한 클래스를 파라미터로 보낸다. true일 경우 이 드라이버를 connect한다.

 

 

isDriverAllowed에서는 아까 메서드를 호출한 클래스에서 이 드라이버 클래스가 존재하는지 확인하고, 존재한다면 jdbc가 static으로 가지고있던 driver와 일치하면 true를 반환해주는것이다.

 

 

메모리로 로드 된다고 해서 인자로 받은 클래스가 객체로 생성되어서 메모리로 로드 되는 것이 아니라 이는 static으로 선언한 변수, 함수에 한해서 메모리 공간 static 영역에 로드된다. 반환할 객체의 클래스가 존재하지않고, 이렇게 스태틱으로 JVM에 올려서 가져오는 유연함을 가진다는 것이다. driver로 다양한 객체들을 가지고 있어야하니까!

 

브리지 패턴: 구현부에서 추상층을 분리하여 각자 독립적으로 변형할 수 있게 하는 패턴. 즉 기능과 구현에 대해 두개의 별도 클래스 가짐

 

서비스 제공자 프레임워크 패턴에는 여러 변형이 있고, 서비스 접근 API는 공급자가 제공하는 것보다 더 풍부한 서비스 인터페이스를 클라이언트에게 반환할 수 있다. 이 예로 브리지 패턴(Bridge pattern)이 있고, 의존성 주입(DI) 프레임워크도 강력한 서비스 제공자라 할 수 있다. 자바 6부터 ServiceLoader라는 범용 서비스 제공자 프레임워크가 제공되어 직접 만들 필요는 없지만 JDBC는 자바 6전이라 사용하지 않는다.

 

정적 팩터리 메서드의 단점 2가지


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

 

Collections 상속을 없는 클래스다. 그래서 딱히 단점이라 말하기 애매하고 이 제약은 상속보다 컴포지션 사용을 유도하며 불변 타입으로 만들려면 이 제약을 지켜야한다는 점에서 장점이 될 수 있다. 상속은 is - a, 컴포지션은 has - a라는 차이가 있고 자세한 내용은 아이템18, 불변타입 관련은 아이템 17을 배우며 다시보려한다.


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

 

생성자는 자바독 상단에 모아서보여주지만, 정적 팩터리 메서드는 api 문서에서 특별하게 다뤄지지않는다. 따라서 클래나 인터페이스 문서상단에 팩토리 메소드에 대한 문서를 제공하는게 좋다. 아래와 같이, SpringApplication.run은 static 메서드고 최상단에 관련된 설명이 나와있다.

 

 

API문서에 바로 안보이니까 정형화된 여러가지 명명방식들이 있다. 

 

명명 규칙 설명 예시
from() 매개변수를 하나 받아 해당 타입의 인스턴스를 반환하는 형변환 메서드 Date d = Date.from(instant);
of() 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드. Set<Rank> faceCards = EnumSet.of(A,B,C);
vallueOf() 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와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다.  FileStore fs = Files.getFileStore(path);
newType() newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. BufferdReader br = Files.newBufferedReader(path);
type() getType과 newType의 간결한 버전 List<Complaint> litany = Collections.list(legacyLitany);

 

정적 팩터리 메서드와 public 생성자는 장단점을 이해하고 사용하고, 무작정 public 생성자를 제공하던 습관을 고치자.

 

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.