[Effective Java] 아이템1 - 생성자 대신 정적 팩터리 메서드를 고려하라

이 글은 Effective Java 책을 읽으면서 중요한 내용 및 알아야 하는 지식들을 정리한 글입니다.

정적 팩터리 메서드란

일반적으로 클래스의 인스턴스를 얻기 위해 생성자를 쓴다.

Boolean bool = new Boolean(true);

이러한 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다. 정적 팩터리 메서드란 객체 생성을 캡슐화하는 기법이다. public 생성자 대신, 혹은 생성자와 함께 instance를 반환하는 메소드를 static 으로 선언하는 것이다.

boolean 기본 타입의 박싱 클래스(boxed class)인 Boolean에서 발췌한 간단한 예다.


public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

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

위 코드를 보면 Boolean 클래스 안에, TRUE, FALSE 정적 변수를 인스턴스화 했고, valueOf 정적 팩토리 메소드를 통해 이 인스턴스를 캐싱해서 반환한다. 즉, new 를 통해 클래스를 생성하지 않는다.

valueOf와 같이 통상적으로 널리 쓰이는 메서드 이름 명명 방식들이 있다.

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
      Date date = Date.from(Instant.now());
    
    • Instant 클래스는 Java8의 Time API이다.
    • An instantaneous point on the time-line.
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
      Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
    
  • valueOf : fromof의 더 자세한 버전
  • instance, getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
  • create, newInstance : 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.
      FileStore fs = Files.getFileStore(path);
    
  • type : getType의 간결한 버전
      List<Complaint> litany = Collections.list(legacyLitany);
    

실무에서 많이 본 메서드들이다.. :sweat_smile: 이러한 정적 팩터리 메서드의 장단점에 대해 알아보자.

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다.

생성자는 매개변수로만 생성자를 생성할 수 있는 반면에, 객체의 특성을 쉽게 묘사한 메서드로 인스턴스를 반환할 수 있다.

// 생성자
BigInteger(int, int, Random)

// 정적 팩터리 매서드
BigInteger.probablePrime()

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

불변 클래스 (immutable class)는 인스턴스를 미리 만들어 놓거나, 생성한 인스턴스를 캐싱하여 재활용하는 방식을 쓸 수 있다. (위에서 설명한 Boolean 처럼)

인스턴스 통제 클래스 (instance-controlleed) 반복되는 요청에 같은 객체를 반환하는 방식으로 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다.

  • sigleton
  • noninstantiable 를 만들 수 있다.

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

다음은 정적 팩토리 메서드를 설명한 포스팅에서 따온 코드이다.

class OrderUtil {

    public static Discount createDiscountItem(String discountCode) throws Exception {
        if(!isValidCode(discountCode)) {
            throw new Exception("잘못된 할인 코드");
        }
        // 쿠폰 코드인가? 포인트 코드인가?
        if(isUsableCoupon(discountCode)) {
            return new Coupon(1000);
        } else if(isUsablePoint(discountCode)) {
            return new Point(500);
        }
        throw new Exception("이미 사용한 코드");
    }
}

class Coupon extends Discount { }
class Point extends Discount { }

할인 코드의 규칙에 따라 Coupon 과 Point 객체를 선택적으로 리턴하고 있다. 위의 예제에서는 class를 리턴하는 것이지만, interface를 리턴하도록 할 수도 있다. 이렇게 하면 어떤 규칙에 따라 어떤 객체를 리턴하는지 구체적인 구현 클래스를 공개하지 않아도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.

자바8부터는 인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀렸다. 따라서 인터페이스를 정적 펙터리의 메서드의 반환 타입으로 사용할 수 있다.

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

3번과 동일한 개념이다. 예제로 EnumSet 클래스를 들면 원소의 수가 64개 이하면 RegularEnumSet 인스턴스를, 65개 이상이면 JumboEnumSet 인스턴스를 반환한다.

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

음.. 이 부분은 예제로 JDBC를 들었는데, 정적 팩터리 메서드를 작성할 시점에 객체의 클래스를 지정하지 않아도 되므로, 유연하다라는 의미인 것 같다.

정적 팩터리 메서드의 단점

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

제목 그대로 정적 팩터리 메서드만 있으면 상속이 불가하다.

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

생성자처럼 new 를 통해 바로 인스턴스를 반환받는 것이 아니라 널리 알려진 규약 및 메서드를 구글링해서 찾아봐야 한다.

Summary: 메소드가 자주 호출되어 캐싱해서 쓸 수 있는 경우, 메소드의 구현이 불변할 경우 (유틸리티 성향의 메소드들) 는 생성자 대신 정적 팩터리 메서드로 구현해도 좋을 것 같다.

TO-DO List

  • Instant 클래스 사용법
  • 장점5번에 대한 이해

참고문서 링크

https://johngrib.github.io/wiki/static-factory-method-pattern

댓글남기기