[Effective Java 3/E] 생성자 대신 정적 팩터리 메서드를 고려하라

2024. 4. 28. 23:03JAVA/Effective Java

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자이다.

다만 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.

예를 들어

  • public 생성
public Book() {

}
  • 정적 팩터리 메서드
public static Book createBook(){
	return instance();
}

 

정적 팩터리 메서드를 정의하자면 객체의 생성을 담당하는 클래스 메서드라고 할 수 있다.

일반적으로 객체를 생성하기 위해서는 new 키워드를 사용하는데 정적 팩터리 메서드의 경우 new를 직접적으로 사용하지 않을 뿐, 클래스 내에 선언되어있는 메서드를 내부의 new를 이용해서 객체를 생성하고 반환하는 것이다.

즉 정적 팩토리 메서드를 통해서 new를 간접적으로 사용한다.

//생성자 방식
String str1 = new String("hello");

//정적 팩토리 메서드 방식
String str2 = String.valueOf("hello");

정적 팩터리 메서드와 생성자를 비교했을 때 장단점이 있다.

우선 장점을 알아보자.

1. 이름을 가질 수 있다.

생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다.

하지만정적 팩터리는 이름을 잘 지으면 반환될 객체의 특성을 쉽게 묘사 가능하다.

//생성자
public class Book{
	private String title;
    	private String author;
    
    public Book(String name,String author){
    	this.name = name;
        this.author = author;
    }
}

// 해당 객체의 특성을 한눈에 알기에 어려움이 있다.
Book book1=new Book("effective java","조슈아 블로크");
//정적 팩토리 메서드
public class Book{
	private String title;
	private String author;

	public static createBook(String title,String author){
		Book book=new Book();
		book.title=title;
        	book.author=author;
		return book;
	}
}

//createBook이라는 이름을 가짐
Book book2=Book.createBook("effective java","조슈아 블로크");

 

※또한 생성자는 메서드 시그니처의 제약이 존재한다 똑같은 타입의 파라미터로 생성자 정의하는 것이 불가능하다.

public Book(String title){
	this.title=title;
}
//위 생성자와 같은 타입의 파라미터로 시그니처 중복오류가 발생한다.
public Book(String author){
	this.author=author;
}

//동일한 타입의 파라미터가 있어도 인스턴스 반환 객체 생성 가능하다.
	public static createBook(String title){
		Book book=new Book();
		book.title=title;
		return book;
	}

	public static createBook(String author){
		Book book=new Book();
		book.author=author;
		return book;
	}

 

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

불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

대표적으로 Boolean.valueOf(boolean) 메서드가 있다.

해당 메서드를 이용하면 객체를 생성 할 때마다, 중복되는 과정을 줄일 수 있고 로직 상에서의 중복(new 로 객체를 매번 생성하여 반환하는 것)을 없앨 수 있다.

// Boolean 클래스는 valueOf 메소드에서 새로 인스턴스를 생성하지 않고, 미리 만들어둔 인스턴스를 재활용한다.
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

=>플라이웨이트 패턴과 유사하다.

→ 특정 객체가 많이 쓰인다면 해당 객체를 계속 생성하기 보단 똑같은 방식으로 제어해 가볍게 해주는 디자인 패턴

 

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

생상자를 사용하면 생성되는 객체의 클래스가 하나로 고정된다.

하지만 정적 팩터리 메서드를 사용하면, 반활할 객체의 클래스를 자유롭게 선택할 수 잇는 유연성을 갖게 된다.

interface Laptop {
    void turnOn();
}

class LowQualityLaptop implements Laptop {
    public void turnOn() {
        System.out.println("느리게 켜진다.");
    }
}

class NormalLaptop implements Laptop {
    public void turnOn() {
        System.out.println("무난한 속도로 켜진다.");
    }
}

class HighEndLaptop implements Laptop {
    public void turnOn() {
        System.out.println("전원 버튼을 누르자마자 부팅이 완료된다.");
    }
}

class Laptops {
    public static Laptop lowQualityLaptop() {
        return new LowQualityLaptop();
    }

    public static Laptop normalLaptop() {
        return new NormalLaptop();
    }

    public static Laptop highEndLaptop() {
        return new HighEndLaptop();
    }
}
이때 사용자는 Laptop의 하위 타입인 lowQualityLaptop, NormalLaptop,HighEndLaptop의 구현체를 직접 알 필요 없다.
Laptop이라는 인터페이스를 사용하면 된다. 여기서 Laptops를 Laptop의 동반 클래스(Companion Class)라고 한다.

 

자바 8버전 부터는 인터페이스가 정적 메서드를 가질 수 있게 되어 동반 클래스는 더이상 필요없어졌다.

인터페이스가 정적 메서드를 갖는 형태로 코드를 작성하면 구체적인 구현체를 사용자에게 공개하지 않고, 반환 타입을 인터페이스로 두게되는데 이렇게 되면 API의 개념적인 무게가 가벼워진다.

개발자는 API를 사용하기 위해 많은 개념을 익히지 않아도 된다. 인터페이스에 명세된 대로 동작한 객체를 얻을 것임을 알기 때문이다.

interface Laptop {
    // 인터페이스가 정적 메서드를 직접 갖는다.
    static Laptop lowQualityLaptop() {
        return new LowQualityLaptop();
    }

    static Laptop normalLaptop() {
        return new NormalLaptop();
    }

    static Laptop highEndLaptop() {
        return new HighEndLaptop();
    }

    void turnOn();
}

class LowQualityLaptop implements Laptop {
    public void turnOn() {
        System.out.println("느리게 켜진다.");
    }
}

class NormalLaptop implements Laptop {
    public void turnOn() {
        System.out.println("무난한 속도로 켜진다.");
    }
}

class HighEndLaptop implements Laptop {
    public void turnOn() {
        System.out.println("전원 버튼을 누르자마자 부팅이 완료된다.");
    }
}

 

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

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

심지어 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다.

예를 들어 클래스 가격에 따라 노트북 인스턴스를 다르게 반환하는 기능을 추가하고 싶다면 아래와 같이 매개변수에 따라 다른 클래스의 인스턴스를 생성해서 반환해주면 된다.

class Laptop {
    static Laptop createByPrice(int price) {
        if (price < 500000) {
            return new LowQualityLaptop();
        }

        if (price < 1500000) {
            return new NormalLaptop();
        }

        return new HighEndLaptop();
    }
}

사용하는 입장에서는 구체적인 타임(하위 타입)객체를 숨길 수도 있다.

HelloService hello = HelloServiceFactory.of("ko");
//-> KoreanHelloService 인지 모름
 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();
      }
   }
}

 

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

public static void main(String[] args) {
      ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
			// 첫번째 구현체를 가져옴(있을수도 없을수도 optional로 가져옴
      Optional<HelloService> helloServiceOptional = loader.findFirst();

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

 

정적 팩터리 메서드의 단점

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

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