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

June·2022년 2월 23일
2

[이펙티브자바]

목록 보기
1/72

배경

@Test
public void isZeroWhenStarted() {
    Location location = new Location();
    ...
}

이펙티브 자바 아이템 1를 참고해보면 좋을 거 같아요 :)

정적 팩터리 메서드

정적 팩터리 메서드는 객체 생성 역할을 하는 클래스 메서드다.

장점1. 이름을 가질 수 있다.

생성자 자체는 생성되는 객체의 특성을 직관적으로 설명하지는 않는다.

    public static Position createStartPosition() {
        return new Position(START_POSITION_VALUE);
    }
Position position = Position.createStartPosition();

이렇게 메서드 명을 지음으로서 어떤 객체가 생성되는지 더 구체적으로 알 수 있다.

또한 생성자는 똑같은 타입을 파라미터로 받는 생성자 두 개를 만들 수 없는데 이때도 정적 팩토리 메서드로 가능하다.

public class Person {
    String name;
    String address;
    
    public Person(String name) {
        this.name = name;
    }
    
    public Person(String address) {
        this.address = address;
    }
    ...
}

위는 불가능하다.

public class Person {
    String name;
    String address;
    
    public Person() {
    }
    
    public Person(String name) {
	    this.name = name;
    }
    
    public static Person withName(String name) {
        return new Person(name);
    }
    
    public static Person withAddress(String address) {
        Person person = new Person();
    	person.address = address;
        return person;
    }
    ...
}

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

public class LottoNumber implements Comparable<LottoNumber> {
    
    public static final int MIN_LOTTO_NUMBER = 1;
    public static final int MAX_LOTTO_NUMBER = 45;
    
    private final int number;
    
    public LottoNumber(final int number) {
        checkLottoNumberRange(number);
        this.number = number;
    }
    
    
    ...
}

로또 게임에서 같은 입력 값에 대해 계속 새 인스턴스를 생성할 필요는 없을 것이다.

public static final int MIN_LOTTO_NUMBER = 1;
public static final int MAX_LOTTO_NUMBER = 45;

public static final LottoNumber[] LOTTO_NUMBER_CACHE = new LottoNumber[MAX_LOTTO_NUMBER + 1];

static {
    IntStream.rangeClosed(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
        .forEach(number -> LOTTO_NUMBER_CACHE[number] = new LottoNumber(number));
}

private final int number;


public static LottoNumber valueOf(final int number) {
    if (number < MIN_LOTTO_NUMBER || number > MAX_LOTTO_NUMBER) {
        throw new IllegalArgumentException("[ERROR]");
    }
    return LOTTO_NUMBER_CACHE[number];
}

인스턴스를 미리 만들어 놓거나 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
생성 비용이 큰 객체일 때 더 좋다.

이렇게 인스턴스가 언제 살아있게 할지 통제하면 장점은?

  • 싱글톤 가능 (아이템3)
  • 인스턴스화 불가로 만들기 가능 (아이템4)
  • 불변 값 클래스에서 동치인 인스턴스가 하나뿐임을 보장 가능 (아이템 17)

코드 출처
우테코 4기 후니 블로그

장점3. 반환 타입의 하위 타입 객체 반환 가능

리턴 타입은 인터페이스로 지정해서 구현체는 노출시키지 않을 수 있다.

public interface Car {

  ...
}
public class ElectricCar {
    int position = 0;
    
    public static Car create(int position) {
        return new ElectricCar(position);
    }
    ....
}

API를 만들 떄 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.

예를 들어 java.util.Collection에서 정적 팩터리 메서드를 통해 45개의 유틸리티 구현체를 제공한다.

인터페이스만 노출되므로 '개념적인 무게 줄이기' 가능. 프로그래머가 알아야하는 개수와 난이도가 줄어든다.

자바 8부터는 public static 메서드를 인터페이스 추가 가능하다. 그래서 굳이 Collecions라는 클래스를 만들지 않고도 인터페이스에 구현 가능하다.
하지만 private static은 자바 9부터 가능하다. 자바9를 쓰면 private static까지 인터페이스에 추가할 수 있으니 java.util.Collection 같은 유틸성 클래스들은 핊요 없어진다.

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

장점 3의 연장선이다. 유연성에 관해 이야기하고 있다.
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.

public class Level {
  ... 
  public static Level of(int score) {
    if (score < 50) {
      return new Basic();
    } else if (score < 80) {
      return new Intermediate();
    } else {
      return new Advanced();
    }
  }
  ...
}

위의 코드에서도 매개변수에 따라 다른 클래스 객체를 반환할 수 있다.

EnumSet 클래스 (아이템36)가 생성자 없이 public static 메서드로 allOf(), of()
등을 제공하는데, 리턴하는 객체의 타입이 enum 타입의 개수에 따라 RegularEnumSet 또는 JumboEnumSet으로 달라진다.

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

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

장점 3, 4와 비슷한 개념이다.

인터페이스나 클래스가 만들어지는 시점에서 하위 타입의 클래스가 존재하지 않아도 나중에 만들 클래스가 기존의 인터페이스나 클래스를 상속 받으면 언제든지 의존성을 주입 받아서 사용가능하다. 반환값이 인터페이스가 되며 정적 팩터리 메서드이 변경없이 구현체를 바꿔 끼울 수 있다.

다른 누군가가 구현하고 있는 코드

public class ElectricCar extends Car {
    ...
}
public class Car {

  	public static Car getCar(int position) {
        Car car = new Car();
        
        // Car 구현체의 FQCN(Full Qualified Class Name)을 읽어온다
        // FQCN에 해당하는 인스턴스를 생성한다.
        // car 변수를 해당 인스턴스를 가리키도록 수정
        
        return car;
    }
}

getCar는 실행시점에 위 코드에 뭐가 적혀있냐에 따라 다르게 작동한다.

JDBC에서 getConnection() 을 쓸 때 실제로 return되어서 나오는 객체는 드라이버마다 다르다 (MySQL 드라이버, Oracle 드라이버..).

서비스 프로바이더 프레임워크는..

  • 서비스 인터페이스 (서비스 구현체 대표)
  • 프로바이더 등록 API (구현체 등록) : DriverManager.registerDriver()
  • 서비스 엑세스 API (클라이언트가 서비스의 인스턴스를 가져갈 때 사용) : DriverManager.getConnection()
  • 서비스 프로바이더 인터페이스 (서비스 인터페이스의 인스턴스 제공): Driver

하지만 사실 JDBC는 서비스 프로바이더 프레임워크를 사용하지 않았다. 서비스 프로바이더 프레임워크는 자바6부터 가능했고, JDBC는 이전에 나왔다.

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

그래서 java.util.Collections는 상속할 수 없다.

상속보다 컴포지션을 사용(아이템18) 도록 유도하고 불변 타입(아이템17)로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점이 될 수도 있기는 하다.

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

생성자는 Javadoc이 자동으로 상단에 모아서 보여준다. 정적 팩토리 메서드는 그렇지 않다.

컨벤션

  • from : 하나의 매개 변수를 받아서 객체를 생성
    • ex. Date date = Date.from(instant);
  • of : 여러개의 매개 변수를 받아서 객체를 생성
  • valueOf: from과 of의 더 자세한 버전
    • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • newInstance | create : 새로운 인스턴스를 생성
  • get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.

0개의 댓글