3 minute read

item1 생성자 대신 정적 팩토리 메소드를 고려하라

정적 팩터리 메소드

  • 생성자 방식을 사용하려면, 하나의 시그니처로는 생성자를 하나만 만들 수 있다.
    • 입력하는 매겨변수를 추가하는 식으로 다르게 할 수도 있지만, 이 경우 코드의 가독성이 떨어지고 개발자도 다른 생성자를 호출하는 실수를 저지를 수 있다.

public class Coffee {
    private int coffeeId;
    private String coffeeName;
    private int shots;

    // 생성자 1: coffeeId와 coffeeName을 받는 생성자
    public Coffee(int coffeeId, String coffeeName) {
        this.coffeeId = coffeeId;
        this.coffeeName = coffeeName;
    }

    // 생성자 2: coffeeId, coffeeName, 그리고 shots를 받는 생성자
    public Coffee(int coffeeId, String coffeeName, int shots) {
        this.coffeeId = coffeeId;
        this.coffeeName = coffeeName;
        this.shots = shots;
    }
}

장점

이름을 가질 수 있다.

생성자를 이용하는 경우, 다른 클래스 혹은 패키지에서 사용하는 경우 어떤 인스턴스 변수인지 알기 어렵다.

Coffee latte = new Coffee(1, "Latte", 2); 
// 1, 2가 어떤 인스턴스 변수에 해당되는 것인지 알기 어렵다.
public class Coffee {
    private int coffeeId;
    private String coffeeName;
    private int shots;

    public Coffee() {
    }

    public Coffee(int coffeeId, String coffeeName, int shots) {
        this.coffeeId = coffeeId;
        this.coffeeName = coffeeName;
        this.shots = shots;
    }

	public static Coffee withCoffeeName(String name){
		Coffee coffee = new Coffee();
		coffee.coffeeName = name;
		return coffee;
	}
}
Coffee latte = Coffee.withCoffeeName("Latte")
// 생성한 인스턴스의 이름 변수가 "Latte"임을 확인할 수 있다.

호출할 때마다 객체를 새로 생성하지 않아도 된다.

import java.util.HashMap;
import java.util.Map;

public class Coffee {
    private int coffeeId;
    private String coffeeName;

    // 캐싱된 Coffee 인스턴스를 저장하는 맵
    private static Map<String, Coffee> coffeeCache = new HashMap<>();

    private Coffee() {
        // private 생성자로 외부에서 인스턴스 생성 불가능하게 함
    }

    // CoffeeName을 기반으로 Coffee 인스턴스를 얻는 정적 메서드
    public static Coffee withCoffeeName(String name) {
        Coffee cachedCoffee = coffeeCache.get(name);
        if (cachedCoffee == null) {
            cachedCoffee = new Coffee();
            cachedCoffee.coffeeName = name;
            coffeeCache.put(name, cachedCoffee);
        }
        return cachedCoffee;
    }

	...
	
}

인스턴스를 미리 만들어 두거나, 이미 이전에 생성하였던 객체를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피한다.

이런 패턴은 객체 생성 비용이 높고, 동일한 객체가 반복적으로 요청되는 상황에서 유용하다. (커피 객체 수가 많아져야 이 패턴의 이점을 확연하게 누릴 수 있다.)

반환 타입의 하위 타입 객체 반환이 가능하다.

리턴 타입을 인터페이스로 지정해 구현 클래스는 노출시키지 않고 객체를 반환해 API를 작게 유지할 수 있다.

public interface Drink{

	...
}
public class Coffee implements Drink{
    private String coffeeName;
	
    public static Drink withCoffeeName(String name) {
        ...
        return new Coffee(name);
    }
	...
	
}

위와 같은 방식으로 구현하면, 실제 구현 클래스 Coffee를 공개하지 않고도 객체를 반환할 수 있다.

이 경우, 프로그래머가 실제 구현체를 찾아보지 않고 인터페이스에 대한 이해를 갖추고 있다면 API를 활용할 수 있다.

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

public interface Drink {
    Drink createDrink(String name);
}

public class Coffee implements Drink {
    @Override
    public Drink createDrink(String name) {
        if (name.equalsIgnoreCase("coffee")) {
            return new Coffee(name);
        }
        throw new IllegalArgumentException("Unsupported drink type");
    }
}

public class Tea implements Drink {
    @Override
    public Drink createDrink(String name) {
        if (name.equalsIgnoreCase("tea")) {
            return new Tea(name);
        }
        throw new IllegalArgumentException("Unsupported drink type");
    }
}

위 코드에서, 상속된 클래스에서 Override한 메서드에 의해, name 값에 따라 서로 다른 클래스의 객체를 반환할 수 있다.

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

인터페이스 혹은 클래스가 만들어지는 시점에, 하위 타입의 클래스가 존재하지 않아도 된다. 추후 구현체에 기존 인터페이스 혹은 클래스를 상속받으면 의존성을 주입할 수 있다.

  • 서비스 제공자 프레임워크 : 서비스의 구현체. (예 : ServiceLoader)
    • 서비스 인터페이스 : 구현체 동작 정의
    • 제공자 등록 API : 제공자가 구현체 등록할 때 사용
    • 서비스 접근 API : 클라이언트가 서비스 인스턴스 얻을 때 사용

ServiceLoader를 사용하면 프레임워크를 직접 구현할 필요가 없이, 로드한 객체에 따라 적절한 동작을 수행한다.


단점

하위 클래스를 만들 수 없다. (상속 불가능)

  • 상속을 위한 public/protected 생성자가 없기 때문에, 하위 클래스를 생성할 수 없다.

프로그래머가 찾기 어렵다.

  • 생성자는 Javadoc에 의해 자동으로 확인이 가능하지만, 정적 팩터리 메소드는 불가능하다.

Leave a comment