본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 1 - 생성자 대신 정적 팩터리 메서드를 고려하라 - 핵심 정리

by 개발인생 2022. 8. 22.
반응형

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

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

생성자 대신정적 팩터리 메서드를 고려하자.

주의할 점은 고려하자 이지 강제하는 것이 아니다.

장점

이름을 가질 수 있다.

가령 주문자에게 긴급건으로 배달을 해야하는 주문이 있다고 하자.

자바에서는 동일한 시그니처의 생성자를 두개 가질 수 없기때문에

아래의 코드는 오류가 발생하게 된다.

public class Order {

   private boolean prime;
   private boolean urgent;

   public Order(boolean prime) {
      this.prime = prime;
   }

   public Order(boolean urgent) {
      this.urgent = urgent;
   }
}

반면 정적 팩토리 메서드를 이용하면 메서드에 이름을 붙일 수 있어

어떤 형태의 주문을 생성할지 정할 수 있게 된다.

public class Order {

   private boolean prime;
   private boolean urgent;

   public static Order primeOrder() {
      Order order = new Order();
      order.prime = true;

      return order;
   }

   public static Order urgentOrder() {
      Order order = new Order();
      order.urgent = true;
      return order;
   }
}

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

정적 팩터리 메서드를 사용하면 매번 호출 될때마다 새로운 인스턴스를 새로 생성하지 않아도 된다.

public class Settings {

   private boolean useAutoSteering;
   private boolean userABS;

   private Settings() {} 

   private static final Settings SETTINGS = new Settings();

   public static Settings getInstance() {
      return SETTINGS;
   }
}

위와 같이 생성자를 private 하게 만들어 해당 클래스에 대한 인스턴스 생성을 막을 수 있습니다.

어디에서나 동일한 값으로 생성되어진 Settings 클래스의 인스턴스를 사용한다면

호출할 때마다 매번 새로운 인스턴스를 만들지 않고

한번 생성되어진 인스턴스를 사용하도록 강제할 수 있습니다.

비슷한 예로 Boolean 클래스의 valueOf 메서드가 있습니다.


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

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

HelloService 라는 인터페이스를 생성.

public interface HelloService {
   String hello();
}

HelloService 를 구현한 클래스 생성.

public class KoreanHelloService implements HelloService {
   @Override
   public String hello() {
     return "안녕";
   }
}
public class EnglishHelloService implements HelloService {
   @Override
   public String hello() {
     return "Hello";
   }
}

해당 인스턴스들을 반환할 팩터리 클래스 생성

public class HelloServiceFactory {

   public static HelloService of(String lang) {
      if (lang.equals("ko")) {
         return new KoreanHelloService();
      } else {
         return new EnglishHelloService();
      }
   }
}

이렇게하면 반환타입에 호환가능한 다른 타입의 인스턴스들을 반환할 수 있게 만들어준다.

인스턴스의 구현체가 아니더라도 상속을 통해 부모 클래스를 만들고 자식 클래스를 반환하는 방법도 얼마든지 가능하다.

또한, 매개 변수에 따라 다른 클래스의 객체를 반환 할 수 있게 만들 수 있다.

   public static void main(String[] args) {
      HelloService hello = HelloServiceFactory.of("ko");

      System.out.println(hello.hello());
   }

위와 같이 인터페이스 기반의 프레임워크를 사용할 수 있도록 강제할 수 있다.

구체적인 구현을 클라이언트로부터 숨길 수 있다.

자바 8 에서는 static 메서드를 인터페이스에 구현할 수 있으니

public interface HelloService {
   String hello();

   static HelloService of(String lang) {
      if (lang.equals("ko")) {
         return new KoreanHelloService();
      } else {
         return new EnglishHelloService();
      }
   }
}

인터페이스를 위와같이 만들어 팩토리 클래스를 따로 생성할 필요없이

인터페이스 생성만으로도 해결이 가능하다.

인터페이스에서는 static만 선언하면 public static으로 간주한다.

물론 private static 메서드 선언도 가능하다.


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

public interface HelloService {
   String hello();
}

HelloService 의 구현체는 없고 인터페이스만 있다고 가정해보자.

   public static void main(String[] args) {
      ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
   }

ServiceLoader 는 서비스 제공자 프레임워크의 자바에서 제공하는 기본 구현체이다.

ServiceLoaderload 라는 정적 팩터리 매소드를 제공한다.

ServiceLoader 는 Iterable을 구현하고 있다.

현재 참조할 수 있는 모든 클래스 패스 내에 있는 등록되어있는 HelloService 의 구현체를 가져온다.

여러개가 있으면 여러개를 다 가져오게 된다.

   public static void main(String[] args) {
      ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);

      Optional<HelloService> helloServiceOptional = loader.findFirst();
   }

구현체가 있을수도, 없을 수도 있기 때문에 Optional 로 가져오게 된다.

   public static void main(String[] args) {
      ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);

      Optional<HelloService> helloServiceOptional = loader.findFirst();

      helloServiceOptional.ifPresent(h -> {
         System.out.println(h.hello());
      });
   }

위와같이 실행하면 등록된 구현체의 메서드를 실행할 수 있게된다.

자세한 동작에 관해서는 생략하도록 한다.

간단히 말하자면 jar에 포함된 구현 파일을 읽어 인터페이스 구현체를 찾고

구현한 인스턴스들을 오브젝트 목록으로 사용하게된다.

이렇게하게 되면 정적 팩토리 메서드가 있는 상태에서 인터페이스만 있으면 된다는 장점이 설명이된다.

팩터리 메서드를 작성하는 시점에 HelloService 의 구현체가 존재하지 않기 때문이다.

위의 코드는 HelloService 의 구현체에 의존적이지 않다.

어떤 구현체가 올지 모르지만 그 구현체가 따르는 인터페이스에 기반하여 코딩하는 경우에 유용하게 사용한다.

예로 JDBC 드라이버가 있다.

어떤 데이터베이스를 사용할지 모르는 상태에서도 코드를 작성할 수 있다.

단점

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

public class Settings {

   private boolean useAutoSteering;
   private boolean userABS;

   private Settings() {}

   private static final Settings SETTINGS = new Settings();

   public static Settings getInstance() {
      return SETTINGS;
   }
}

위의 클래스는 private 생성자를 사용하기 때문에 상속을 할 수 없다.

물론 우회해서 사용하는 방법이 있다.

public class AdvancedSettings {

   Settings settings;
}

위처럼 Settings 를 델리게이션으로 가지고 있고 AdvancedSettings 에서

Settings 를 사용하면 굳이 상속을 받지 않아도 Settings 의 기능을 사용할 수 있으니 오히려 장점이 될 수 있다.

또한, 정적 팩토리를 제공하면서 생성자를 제공하는 경우도 있다.

예로 ArrayList 가 있다.

   public static void main(String[] args) {
      List<String> list = new ArrayList<>(); --- 생성자 사용
      List.of("aaa", "bbb", "ccc"); --- 정적 팩토리 메서드 사용
   }

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

만약 정적 팩터리 메서드 외에 다른 메서드들이 많다면 프로그래머들이 정적 팩터리 메서드를 찾아 사용하기 힘들어진다.

문서 정리에 좀 더 신경을 써서 사용자가 한번에 찾아볼 수 있도록 작성해야한다.

또한 헷갈리지 않도록 일종의 명명규칙을 사용하도록 하자.

  • from : 매개 변수 하나를 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
    • Date d = Date.from(instant);
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
    • Set faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf : from 과 of의 더 자세한 버전
    • BigInteger prime = BinInteger.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: newInstance 와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 떄 쓴다. "Type" 은 팩터리 메서드가 반환할 객체의 타입이다.
    • BufferedReader br = Files.newBufferedReader(path);
  • type : getType과 newType의 간결한 버전
    • List litany = Collections.list(legacyLitany)
반응형

댓글