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

2020. 8. 8. 14:24기술 서적/Effective Java 3∕E

Effective Java 3/E


정적 팩터리 메서드 (static factory method)

클래스의 인스턴스를 반환하는 단순 정적 메서드.

public 생성자를 사용해서 객체를 생성하는 방법 말고 public static 팩터리 메서드를 사용해서 해당 클래스의 인스턴스를 만드는 방법이 있다. 이러한 방법에는 아래와 같은 장단점이 있다.

 

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

public class Foo {

    String name;

    public Foo(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Foo foo = new Foo("lydia");
    }

}

생성자는 클래스이름과 동일한 이름을 갖는다.

main문 안의 Foo()만 보면 생성자의 매개변수로 String을 넣었을 때 해당 문자열이 이름이 될 지, 주소가 될 지 사용자는 알 수 없다.

 

public class Foo {

    String name;

    public Foo(String name) {
        this.name = name;
    }

    public static Foo withName(String name) {
        return new Foo(name);
    }

    public static void main(String[] args) {
        Foo foo = Foo.withName("lydia");
    }

}

정적 팩터리 메서드를 사용 할 경우에는 withName() 처럼 메서드에 이름을 지정할 수 있어서 매개변수로 보내는 문자열이 이름으로 쓰인다는 것을 쉽게 알 수 있다.

 

public class Foo {

    String name;
    String address;

    public Foo(String name) {
        this.name = name;
    }

    public Foo(String address) { // 불가능!
        this.address = address;
    }

    public static void main(String[] args) {
        Foo foo1 = new Foo("lydia");
        Foo foo2 = new Foo("seoul"); // 불가능!
    }

}

이름을 가질 수 있다는 것은 시그니처를 구분할 수 있다는 의미기도 하다.

메서드 시그니처는 메서드 이름과 입력 매개변수(parameter)의 타입들로 이루어지는데, 앞서 말한대로 생성자는 클래스와 동일한 이름을 가지기 때문에 여러개 만들어도 이름은 클래스 이름으로만 만들어진다.

따라서 생성자의 시그니처는 매개변수 뿐이기 때문에 동일한 타입을 매개변수로 받는 생성자를 여러 개 만들 수 없다.

Foo 클래스의 필드 name, address는 둘 다 String 타입이기 때문에 위처럼 name만 받는 생성자, address만 받는 생성자를 따로 만들 수 없다는 뜻이다.

 

public class Foo {

    String name;
    String address;

    public Foo() {
        super();
    }

    public static Foo withName(String name) {
        Foo foo = new Foo();
        foo.name = name;
        return foo;
    }

    public static Foo withAddress(String address) {
        Foo foo = new Foo();
        foo.address = address;
        return foo;
    }

    public static void main(String[] args) {
        Foo foo1 = Foo.withName("lydia");
        Foo foo2 = Foo.withAddress("seoul");
    }

}

정적 팩터리 메서드는 이름을 가질 수 있기 때문에, 동일한 문자열 타입인 name, address를 받는 메서드 withName()withAddress()를 구분해서 만들 수 있다.

 

 

장점 2. 반드시 새로운 객체를 만들 필요가 없다.

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

b가 true인 경우에는 Boolean에 정의되어있는 상수인 Boolean.TRUE를 반환하기 때문에 매번 새로운 객체를 만들어서 반환하지 않는다.

불변(immutable) 클래스인 경우나 매번 새로운 객체를 만들 필요가 없는 경우 미리 만들어둔 인스턴스를 반환하면 된다.

 

public class Foo {

    String name;
    String address;
    public static final Foo INSTANCE = new Foo();

    public Foo() {
        super();
    }

    public static Foo getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Foo foo = Foo.getInstance();
    }

}

위와 같이 클래스 내에 static final로 정의된 INSTANCE라는 이름의 객체를 반환하는 정적 팩터리 메서드 getInstance()를 만들면,

new Foo()처럼 매번 새로운 객체를 만들어서 보내는 것이 아니라, 메서드 호출 시 이미 클래스 내에 static final로 정의된 INSTANCE 객체 하나만을 보낼 수 있다.

 

 

장점 3. 리턴 타입의 하위 타입 인스턴스를 만들 수도 있다.

자바 8부터 인터페이스에 정적 메서드 정의가 가능해졌다.

public interface FooInterface {
    public static Foo getFoo() {
        return new Foo();
    }
}

이를 통해 리턴 타입을 인터페이스로 지정하면, 인터페이스의 구현체 Foo를 노출시키지 않고 구현체의 인스턴스 new Foo()를 만들어 줄 수 있게 되었다.

java.util.Collections라는 클래스를 굳이 만들지 않고도 인터페이스 자체에 구현할 수 있다.

자바 9의 List 인터페이스의 of() 메서드는 인터페이스를 반환하는 정적 팩터리 메서드이다.

클라이언트는 인터페이스의 중재로 인해 반환받은 객체가 실제로 어떤 타입인지 알 필요가 없다.

정적 팩터리 메서드가 알아서 적절한 객체를 인터페이스에 담아주기 때문이다.

 

static <E> List<E> of() {
    return (List<E>) ImmutableCollections.ListN.EMPTY_LIST;
}

자바 7 이하에서는 인터페이스에 정적 메서드를 선언할 수 없었다. 따라서 인터페이스의 유사 클래스를 만들어 그 안에 정적 메서드를 정의하는 방식으로 우회했다.

아래 자바 7의 Collections 클래스의 emptyList() 메서드가 그 예이다. emptyList() 메서드 또한 비어있는 불변 리스트를 리턴한다.

 

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}

 

 

장점 4. 리턴하는 객체의 클래스가 입력 매개변수에 따라 매번 다를 수 있다.

public class Foo {

    public Foo() {
        super();
    }

    public static Foo getFoo(boolean flag) {
        return flag ? new Foo() : new BarFoo();
    }

    static class BarFoo extends Foo {
    }

    public static void main(String[] args) {
        Foo foo1 = Foo.getFoo(true); // Foo
        Foo foo2 = Foo.getFoo(false); // BarFoo
    }

}

매개변수 flag에 따라서 Foo의 인스턴스가 반환될 수도, Foo를 상속받는 BarFoo의 인스턴스가 반환될 수도 있다.

 

public class Foo {

    enum Color {
        RED, BLUE, WHITE
    }

    public static void main(String[] args) {
        EnumSet<Color> colors = EnumSet.allOf(Color.class);
        EnumSet<Color> blueAndWhite = EnumSet.of(Color.BLUE, Color.WHITE);
        System.out.println(colors); // [RED, BLUE, WHITE]
        System.out.println(blueAndWhite); // [BLUE, WHITE]
    }

}

EnumSet 클래스는 public 생성자 없이 public static 메서드 allOf(), of()등을 제공한다.

allOf() 메서드는 지정된 타입의 모든 요소를 갖는 EnumSet을 만들어 반환한다.

위의 예제에서는 Color.class의 모든 요소를 갖는 EnumSet[RED, BLUE, WHITE]를 반환했다.

of()메서드는 매개변수로 보낸 요소들을 갖는 EnumSet을 만들어 반환한다.

위의 예제에서는 Color.BLUE, Color.WHITE를 갖는 EnumSet[BLUE, WHITE]를 반환했다.

 

public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
    EnumSet<E> result = noneOf(elementType);
    result.addAll();
    return result;
}
public static <E extends Enum<E>> EnumSet<E> of(E e) {
    EnumSet<E> result = noneOf(e.getDeclaringClass());
    result.add(e);
    return result;
}

두 메서드 모두 내부에서 noneOf() 메서드를 사용하는데, 먼저 비어있는 EnumSet을 반환하는 noneOf() 메서드를 통해서 객체를 하나 생성 한 후에 거기에 모든 요소를 넣거나 / 매개변수로 온 요소들을 넣는다.

 

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);
}

noneOf()메서드는 이렇게 입력 매개변수의 길이에 따라서 추상클래스인 EnumSet을 구현한 RegularEnumSet 또는 JumboEnumSet 클래스 객체를 반환한다.

하지만 장점 3번과 같은 맥락으로, 이 두 객체 타입은 노출되지 않고 감춰져 있기 때문에 사용자는 이에 대해 알 필요가 없으며 추후에 새로운 타입을 만들거나 기존 타입을 없애도 문제되지 않는다.

 

 

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

서비스 제공자 프레임워크를 구성하는 핵심 컴포넌트

  • 서비스 인터페이스(service interface) : 구현체의 동작을 정의
  • 제공자 등록 API(provider registration API) : 제공자가 구현체를 등록할 때 사용
  • 서비스 접근 API(service access API) : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
  • 서비스 제공자 인터페이스(servic provider interface) : 서비스 인터페이스의 인스턴스를 제공
  • 클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있다.
    조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환한다.
  • 서비스 제공자 인터페이스가 없다면 리플렉션을 사용해 각 구현체를 인스턴스로 만든다.
서비스 제공자 프레임워크
제공부 서비스부
서비스 제공자 인터페이스
(service provider interface)
Driver
제공자 등록 API
(provider registration API)
DriverManager.registerDriver()
서비스 인터페이스
(service interface)
Connection
서비스 접근 API
(service access API)
Driver.Manager.getConnetion()

JDBC에서 DriverManager.getConnection()라는 정적 팩터리 메서드가 서비스 접근 API 역할을 한다.

getConnection()메서드는 Connection이라는 서비스 인터페이스를 반환하는데,

제공자 등록 API 역할을 하는 DriverManager.registerDriver()메서드로 등록한 제공자(Driver)에 맞는 Connection 서비스를 반환한다.

mySql Driver, Oracle Driver 등 DB에 따라 다른 Connection을 제공한다는 뜻이다.

registerDriver()메서드는 Driver가 로드되는 static 시점에 호출된다.

getConnection()메서드는 호출 되기 전, 미리 등록된 제공자가 있다고 확신하고 그에 맞는 Connection 서비스 구현체를 반환하도록 약속되어있다.

 

 

단점 1. public 또는 protected 생성자 없이 정적 팩터리 메서드만 제공하는 클래스는 하위 클래스를 만들 수 없다.

Collections 프레임워크에서 제공하는 유틸리티 구현 클래스(java.util.Collections)는 상속할 수 없다.

이 제약은 컴포지션 사용을 유도하고, 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점일 수 있다.

 

 

 

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

생성자는 API Docs의 상단에 모아놨기 때문에 찾기가 쉽다. 하지만 정적 팩터리 메서드는 다른 메서드와 구분 없이 함께 보여준다.

따라서 사용자가 정적 팩터리 메서드 방식 클래스를 사용할 때 인스턴스화 할 방법을 알아서 찾아내야한다.

다음은 정적 팩터리 메서드에서 흔히 사용하는 명명 방식이다.

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

 

 


privae static 메서드가 필요한 이유

public static void doSomethingToday() {
    // TODO 치과에 간다
    dailyRoutine();
}

public static void doSomethingTomorrow() {
    // TODO 마트에 간다
    dailyRoutine();
}

private static void dailyRoutine() {
    // TODO 자바 공부를 한다 
    // TODO 맛있는걸 먹는다
}

오늘 할 일인 doSomethingToday() 메서드와 내일 할 일인 doSomeethingTomorrow() 메서드가 두 개 있다.

매일 할 일들 각 메서드에 넣으면 중복되기 때문에 코드를 따로 뽑아서 dailyRoutine()이라는 메서드로 만들었다.

그리고 매일 할 일은 공개하고 싶지 않아 private로 처리하고 싶다.

이 때, 매일 할 일 메서드를 호출하고 있는 오늘 할 일 메서드와 내일 할 일 메서드가 static으로 선언되어있다면, 해당 scope안에 속해있는 매일 할 일 메서드 또한 static이어야 한다.

 

 


참고 자료