본문 바로가기

이펙티브 자바

[이펙티브 자바] 아이템 1. 생성자 대신 정적 팩토리 메서드를 고려하라

인스턴스 : 클래스(설계도)를 바탕으로 만들어진 구체적인 객체

public 생성자 : 클래스의 인스턴스를 생성할 때 초기화를 담당하는 특별한 메서드

public class Car {
    private String color; // Car 클래스의 필드

    // Car 클래스의 public 생성자
    public Car(String color) {
        this.color = color; // 생성자를 통해 Car 인스턴스의 색상을 초기화
    }
    
    // Car 클래스의 메서드
    public void drive() {
        System.out.println(color + " 차가 달립니다.");
    }
}

// Main 클래스에서 Car 클래스의 인스턴스 생성 및 사용
public class Main {
    public static void main(String[] args) {
        Car myCar = new Car("빨간색"); // Car 클래스의 인스턴스를 생성
        myCar.drive(); // "빨간색 차가 달립니다." 출력
    }
}

위 예시에서 Car 클래스에는 색상(color)를 초기화하는 public 생성자가 있다. Main 클래스에서는 이 생성자를 사용해 Car 클래스의 인스턴스를 생성하고, drive 메서드를 호출한다. 이처럼 public 생성자를 통해 클래스 외부에서 인스턴스를 쉽게 생성하고 사용가능하다.

 

정적 팩토리 메서드는 클래스의 인스턴스를 반환하게하는 정적 메서드이다. 이 방법은 클래스의 인스턴스를 생성하고 반환하기 위한 'new' 키워드를 직접사용하지않고, 클래스 내부에 정의된 특정 메서드를 통해 객체의 인스턴스를 얻는 방식이다.

 

정적 팩토리 메서드의 예시 : 

public class Boolean {
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
}

 

같은 코드를 public 생성자로 사용했을 때 : 

public class Boolean {
    private boolean value;

    // Public 생성자
    public Boolean(String s) {
        this.value = Boolean.parseBoolean(s);
    }

    // valueOf 메서드 대신 사용하는 생성자
    // Boolean myBool = new Boolean("true");

    // 나머지 클래스 구현
}

 

 

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

1. 이름을 가질 수 있다.

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

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

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

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

이 모든 장점을 하나의 코드안에 담아보았다 : 

public class VehicleFactory {

    private static final Map<String, Vehicle> cache = new HashMap<>();

    // 1. 이름을 가질 수 있음: 메서드 이름을 통해 반환되는 객체의 의도를 명확히 할 수 있습니다.
    public static Vehicle createElectricCar() {
        return new ElectricCar();
    }

    // 2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 됨: 인스턴스 캐싱을 통해 불필요한 객체 생성을 방지합니다.
    public static Vehicle getBicycleInstance() {
        // 캐시에서 인스턴스를 검색, 없으면 생성하여 캐시에 추가
        return cache.computeIfAbsent("bicycle", k -> new Bicycle());
    }

    // 3. 반환 타입의 하위 타입 객체를 반환할 수 있음: 인터페이스 Vehicle의 하위 타입 반환
    public static Vehicle createVehicle(String type) {
        switch (type) {
            case "electric":
                return new ElectricCar();
            case "bicycle":
                return new Bicycle();
            default:
                throw new IllegalArgumentException("Unknown vehicle type");
        }
    }

    // 4. 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있음: createVehicle 메서드 참조
    // (위에서 이미 설명함)

    // 5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됨:
    // 이 예시에서는 ElectricCar와 Bicycle 클래스가 이 메서드들을 사용하는 클라이언트 코드보다 나중에 작성될 수 있습니다.

    private interface Vehicle {
    }

    private static class ElectricCar implements Vehicle {
    }

    private static class Bicycle implements Vehicle {
    }
}

 

정적 팩토리 메서드의 단점도 존재한다.

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

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

2번 단점을 보완할 수 있는 방법이있다. 바로 널리 알려진 규약을 따라 메서드 이름을 짓는것이다.

대표적인 명명방식들이 있다. 예를 들면 : 

 

  1. valueOf - 주어진 매개변수를 가지고 해당 타입의 인스턴스를 반환합니다. 주로, 기본 데이터 타입의 래퍼 클래스에서 많이 볼 수 있습니다.
    • 예: Integer.valueOf(int i)
  2. of - valueOf와 유사하지만, 더 간결한 버전으로 사용됩니다.
    • 예: List.of("a", "b", "c"), EnumSet.of(ChronoField.DAY_OF_WEEK)
  3. getInstance - 인스턴스를 가져오되, 같은 인스턴스임을 보장하지 않습니다. 싱글턴 패턴이나 인스턴스 캐싱 시 사용될 수 있습니다.
    • 예: Calendar.getInstance()
  4. getSingleton - getInstance와 유사하지만, 항상 같은 인스턴스를 반환함을 보장합니다. 주로 싱글턴 패턴 구현에 사용됩니다.
    • 예: Runtime.getRuntime()
  5. newInstance - getInstance와 유사하지만, 매번 새로운 인스턴스를 생성해 반환합니다. 반복 사용 시 다른 인스턴스를 기대할 때 사용합니다.
    • 예: Array.newInstance(Class<?> componentType, int length)
  6. getType - getInstance와 유사하지만, 반환되는 인스턴스의 타입이나 클래스에 더 집중할 때 사용됩니다.
    • 예: Files.getFileStore(Path path)
  7. newType - newInstance와 유사하지만, 새로운 인스턴스를 생성할 때 사용되며, 생성되는 인스턴스의 타입을 더 명확히 하고자 할 때 사용됩니다.
    • 예: Collections.newSetFromMap(Map<E, Boolean> map)

 

종합 : 정적 팩토리 메서드와 public 생성자는 각자 쓰임새가 있지만 정적 팩토리 메서드를 사용하는게 유리한 경우가 더 많으므로 정적팩토리 멕서드사용을 지향하자.