[Effective Java] 2장 객체 생성과 파괴 - 아이템 1. 생성자 대신 정적 팩터리 메소드를 고려하라

배상규·2023년 9월 10일
0

이펙티브 자바

목록 보기
1/12
post-thumbnail

개요

일반적으로 클래스의 인스턴스를 얻는 방법으로는 public 생성자다.

public class Product {
    private String name;

    private int price;

    private String description;

    private ProductStatus productStatus;
    
    public Product(String name, int price, String description, ProductStatus productStatus){
        this.name = name;
        this.price = price;
        this.description = description;
        this.productStatus = productStatus;
    }


}

public enum ProductStatus{
    WAITING,
    RELEASE,
    DISCONTINUE
}

일반적인 클래스에선 public 생성자만응로도 충분히 생성이 가능하지만, 생성자 외에 정적 팩터리 메서드를 제공하면 사용자 입장에서 의도한 대로 인스턴스를 만들기 쉬워지는 경우가 종종 있다.

정적 팩터리 메서드의 대표적인 예시는 BooleanvalueOf() 메서드가 있다.

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

메서드는 기본 타입인 boolean값을 받아와 Boolean 객체로 만들어 반환하고 있다.


정적 팩터리 메서드의 장점

이름을 가질 수 있다.

책 에서는 BinInteger(int, int, Random)과 정적 팩터리 메서드인 BigInteger.probablePrime중 어느 쪽이 '값이 소수인 BigInteger를 반환하다'는 의미인지 생각해 보라고 한다. 앞서 작성한 public 생성자 코드를 예로 예제를 만들어 보자면

만약 Product클래스의 메인 생성자인 (name, price, description, productStatus)만 보고 어떤 특성을 가진 Product인지 파악하기 어렵다.

또한, 하나의 시그니처로는 하나의 생성자를 만들 수 있는데, 정적 팩터리 메서드는 이름을 가질 수 있으므로 하나의 시그니처로 여러 개의 정적 팩터리 메서드를 만들어 인스턴스를 반환할 수 있다.

public class Product {
    private String name;

    private int price;

    private String description;

    private ProductStatus productStatus;

    public Product(String name, int price, String description, ProductStatus productStatus){
        this.name = name;
        this.price = price;
        this.description = description;
        this.productStatus = productStatus;
    }

    public static Product waitingMember(String name, int price, String description) {
        return new Product(name, price, description, ProductStatus.WAITING);
    }

    public static Product releaseMember(String name, int price, String description) {
        return new Product(name, price, description, ProductStatus.RELEASE);
    }

    public static Product discontinueMember(String name, int price, String description) {
        return new Product(name, price, description, ProductStatus.DISCONTINUE);
    }

}

public enum ProductStatus{
    WAITING,
    RELEASE,
    DISCONTINUE
}

위와 같은 생성자로 ProductStatus를 구분하는 것보단 같은 행위를 가진 여러개의 정적 팩터리 메서드를 만들면, 사용자 입장에선 혼동의 여지 없이 특정 상태의 Product 인스턴스를 생성할 수 있게 된다.


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

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

Boolean의 valueOf() 메소드는 인스턴스를 미리 캐싱해뒀다가 반환해 주는 것을 알 수 있다. 이러한 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능을 상당히 높여줄 수 있고, 플라이웨이트 패턴도 이와 비슷한 기법으로 볼 수 있다.


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

  String[] strs = {"alpha", "beta", "charlie"};
  List<String> lst = Arrays.asList(strs);

해당 코드는 간단하게 문자열 배열을 list로 변환하는 코드이 이다 이런 Arrays유틸 클래스의 asList() 메서드를 사용해본 적이 있다면 해당 장점을 이해할 수 있을 것이다.

public static <T> List<T> asList(T... a) {
   return new ArrayList<>(a);
}

실제 asList 구현 코드를 확인하면 List의 하위 구현체인 ArrayList로 값을 래핑하여 반환하는데, 이때 사용자는 이러한 구현체까지 알 필요가 없다. 즉 반환 객체의 클래스를 자유롭게 선택할 수 있다는 유연성은 개발자가 구현체를 공개하지 않고 구현체를 반환할 수 있기에 API를 작게 유지할 수 있다.


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

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다. 이점은 파라미터의 값에 따랄 다른 하위 타입을 반환할 수 있도록 한다.
만약 어떤 값에 따라 ProductStatus를 반환하고 싶다면, 정적 팩터리를 만들고 그 안에 비교 로직을 세우면 될 것이다.

public enum ProductStatus{
    WAITING(0,30),
    RELEASE(31,60),
    DISCONTINUE(61,100);

    private final int min;
    private final int max;

    ProductStatus(int min,int max){
        this.min = min;
        this.max = max;
    }

    public static ProductStatus of(int num){
        return Arrays.stream(values())
                .filter(checkProductStatus(num))
                .findFirst()
                .orElseThrow(()-> new NoSuchElementException("해당 ProductStatus를 찾을 수 없습니다."))
    }

    private static Predicate<ProductStatus> checkProductStatus(int num){
        return ele -> ele.min <= num && ele.max >= num;
    }
}

정적 팩토리 메서드 단점

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

하지만 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만드려면 이 제약을 지켜야 하기에 오히려 장점으로 받아들일 수 도있다.


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

생성자 처럼 API 설명에 명확히 드러나지 않기에 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다. 그렇기 때문에 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 사용하여 문제를 완화하여야 한다.

정적 팩터리 메서드 명명 방식

  1. form : 매개변수를 하나 받아서 인스턴스 반환하는 형변환 메서드 ex) Date d = Date.from(instant)
  2. of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환 집계 메서드 ex) Set faceCards = EnumSet.of(JACK, QUEEN, KING);
  3. valueOf : from과 of의 더 자세한 버전 ex) BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  4. instance 혹은 getInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다. ex) StackWalker luke = StackWalker.getInstance(options);
  5. create 혹은 newInstance : 매개변수를 하나 받아서 인스턴스 반환하는 형변환 메서드 ex) Object newArray = Array.newInstance(classObject, arraylen);
  6. getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. ex) FileStore fs = Files.getFileStore(path);
  7. newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. ex) BufferedReader br = Files.newBufferedReader(path);
  8. type :getType과 newType의 간결한 버전 ex) List litany = Collections.list(legacyLitany);
profile
기록에 성장을

0개의 댓글