본문 바로가기

IT/자바

[Effective Java] Item 1 생성자 대신 정적 팩토리 메서드를 고려하라

반응형

Item 1 생성자 대신 정적 팩토리 메서드를 고려하라

 

클래스의 인스턴스를 얻는 기본적인 방법은 public 생성자이다.

하지만 public 생성자 대신 정적 팩터리 메서드를 사용함으로써 많은 장점을 얻을 수 있다.

 

장점 1

이름을 가질 수 있다.

-> 일반적인 public 생성자를 사용할 경우 해당 클래스의 이름밖에 사용할 수 없지만,

정적 팩터리 메서드를 사용하면 호출하고자 하는 객체마다 이름을 정할 수 있기에 반환되는 객체의 역할을 정확히 파악할 수 있다.

 

예를 들어 

1. 일반 public 생성자

 public BigInteger (int bitLength, Random rnd) {
        if (bitLength < 2)
            throw new ArithmeticException("bitLength < 2");

        return (bitLength < SMALL_PRIME_THRESHOLD ?
                smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
                largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
    }

2. 정적 팩터리 메소드

 public static BigInteger probablePrime(int bitLength, Random rnd) {
        if (bitLength < 2)
            throw new ArithmeticException("bitLength < 2");

        return (bitLength < SMALL_PRIME_THRESHOLD ?
                smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
                largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
    }

위 2개의 인스턴스를 반환하는 메소드 중에 어느 코드가 반환하려는 객체가 소수인지 파악하는데 쉬운 코드일까 ? 

2번째 정적 팩터리 메소드 BigInteger.probablePrime()으로 호출 함으로써 개발자는 소스 더 수월해진다.

 

장점 2

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

statc 메소드임으로 호출 때마다 새로운 인스턴스가 생성되지 않는다. 이러한 클래스를 인스턴스 통제 클래스라고도 부른다.

 

인스턴스 통제를 할 경우 아래의 3가지의 의미를 갖게 된다.

1. 싱글턴 객체

2. 인스턴스화 불가

3. 동치인 인스턴스가 단 하나뿐임을 의미 (a == b일 때만 a.equals(b)가 성립)

 

장점 3

반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. (유연성 UP)

구현 클래스를 공개하지 않고도 다른 클래스의 객체를 반환할 수 있음으로 API를 작게 유지할 수 있는 장점이 있다.

(이는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 함)

 

자바 8 컬렉션 구조를 보면 인스턴스화 불가 클래스인 Collections에서 정적 팩터리 메서드를 통해 아래 표에 있는 하위 타입 객체들을 반환받을 수 있다.

 

장점 4

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

예를 들어 EnumSet 클래스 같은 경우 관리하는 원소의 갯수가 64개 이하인 경우 ReqularEnumSet 객체를 반환하고, 64개 이상인 경우 JumboEnumSet 객체를 반환한다.

클라이언트는 EnumSet 클래스를 사용하면서 ReqularEnumSet, JumboEnumSet 존재를 모르더라도 사용하는데 어려움이 없을 것이다.

 

장점 5

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

위 장점은 서비스 제공자 프레임워크를 만드는 근간이 된다. 대표적으로 JDBC가 있다.

 

서비스 제공자 프레임워크는 4개의 컴포넌트로 이뤄지는데

1) 구현체의 동작을 정의하는 서비스 인터페이스 - Connection

2) 구현체를 등록할 때 사용하는 제공자 등록 API - DriveManager.registerDirver

3) 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API - DriverManager.getConnection

4) 서비스 제공자 인터페이스 (선택) - Dirver

 


단점 1

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

이 부분은 컴포지션을 사용하도록 유도하고 불변 타입으로 만들 경우 장점이 될 수 있다.

 

단점 2

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

 

명명 규칙

  • from : 매개변수를 하나 받아 해당 타입의 인스턴스를 반환하는 형 변환 메서드
    ex) Date d = Date.from(instant);
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
    ex) Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf : from과 of의 더 자세한 버전
    ex) BingInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance / getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
    ex) StackWalker luke = StackWalker.getInstance(options);
  • create / newInstance : instance / getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
    ex) Object newArray = Array.newInstance(calssObject, arrayLen);
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 객체의 타입이다.
    ex) FileStore fs = Files.getFileStore(path);
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 객체의 타입이다.
    ex) BufferedReader br = Files.newBufferedReader(path);
  • type : getType과 newType의 간결한 버전
    ex) List<Complaint> litany = Collections.list(legacyLintany);

정리

정적 팩터리 메서드와 public 생성자는 각각의 쓰임새가 있음으로 장단점을 이해하고 사용하는 것이 좋으며, 

정적 팩터리 메서드의 장점이 많음으로 무작정 public 생성자만 사용하지 말자

반응형