독서

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

오렌지색 귤 2022. 1. 18. 16:00
반응형

Quiz

질문 1. true, false를 나타내는 인스턴스를 매번 생성해야하는 문제는 어떻게 해결할 수 있을까?

 

 

 

정답

 

아래 코드와 같이 Boolean 클래스 내에 선언된 static final 변수를 가져오면 된다.

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

 

 

질문 2. 아래 테스트 코드의 결과는 true 인가 false 인가?

public static void main(String[] args) throws IOException {

        Boolean bool = true;
        Boolean bool2 = true;

        Boolean bool3 = Boolean.TRUE;
        Boolean bool4 = Boolean.TRUE;

        Boolean bool5 = Boolean.valueOf(true);
        Boolean bool6 = Boolean.valueOf(true);

        System.out.println(bool == bool2);
        System.out.println(bool2 == bool3);
        System.out.println(bool3 == bool4);
        System.out.println(bool4 == bool5);
        System.out.println(bool5 == bool6);
        System.out.println(bool6 == bool);
    }

 

 

 

 

정답

 

모두 다 true를 반환한다. Boolean은 불변 클래스이다.


본 내용

public 생성자가 아닌 정적 팩터리 메서드의 장점과 단점에 대해 간단히 알아보자

 

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

 

// public 생성자를 통한 객체 생성
Car myNewCar = new Car("현대", "제네시스", "G80");

// 정적 팩터리 메서드 사용
Car myNewCar = Car.create("현대", "제네시스", "G80);

 

위의 코드에서 알 수 있듯이 자동차가 생성된다는 의미를 메서드 명에 담을 수 있다. 코드의 가독성이 높아지는 결과를 낳을 것이다.

 

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

 

앞선 퀴즈에서 질문 2의 코드를 보도록 하자.


불리언 객체가 필요할 때마다 new 연산자를 통해 객체를 만들어 반환했다면 메모리 상의 낭비가 심할 것이다.


Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다.

 

아래 Boolean 클래스의 코드를 보면 이해할 수 있다.

 

// 불변 클래스인 Boolean
public final class Boolean implements java.io.Serializable, Comparable<Boolean> {

    // 인스턴스를 미리 만들어 둔다
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    private final boolean value;

    // public 생성자는 위의 value에 값을 할당
    public Boolean(boolean value) {
        this.value = value;
    }

    // String으로 받아와서 기본 생성자에 넣어주기
    public Boolean(String s) {
        this(parseBoolean(s));
    }

    // 문자열 true에서 대소문자는 신경 쓰지 않는다
    public static boolean parseBoolean(String s) {
        return ((s != null) && s.equalsIgnoreCase("true"));
    }

    // 미리 만들어둔 인스턴스 반환
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

    // 미리 만들어둔 인스턴스 반환
    public static Boolean valueOf(String s) {
        return parseBoolean(s) ? TRUE : FALSE;
    }
    ... // 이하 생략

 

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

 

이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물한다.


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


이는 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 하다.

 

자바 8 이전

 

인터페이스에 정적 메서드를 선언할 수 없었다


그렇기 때문에 이름이 "Type"인 인터페이스를 반환하는 정적 메서드가 필요하면, "Types"라는 (인스턴스 불가인) 동반 클래스(companion class)를 만들어 그 안에 정의하는 것이 관례였다.

 

대표적인 예로 Collection 인터페이스와 Collections 동반 클래스가 있다

 

Collection<String> empty = Collections.emptyList();

 

컬렉션 프레임워크는 45개의 클래스를 공개하지 않기 때문에 API 외견을 훨씬 작게 만들 수 있었다.


프로그래머는 명시한 인터페이스대로 동작하는 객체를 얻을 것임을 알기에 굳이 별도 문서를 찾아가며 실제 구현 클래스가 무엇인지 알아보지 않아도 된다.


나아가 정적 팩터리 메서드를 사용하는 클라이언트는 얻은 객체를 인터페이스만으로 다루게 된다.

 

public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean removeAll(Collection<?> c);

    ... // default method 생략
}
public class Collections {
    private Collections() {
    }

    public static final List EMPTY_LIST = new EmptyList<>();

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

    private static class EmptyList<E>
        extends AbstractList<E>
        implements RandomAccess, Serializable {
        private static final long serialVersionUID = 8842843931221139166L;

        public Iterator<E> iterator() {
            return emptyIterator();
        }
        public ListIterator<E> listIterator() {
            return emptyListIterator();
        }

        public int size() {return 0;}
        public boolean isEmpty() {return true;}

        public boolean contains(Object obj) {return false;}
        public boolean containsAll(Collection<?> c) { return c.isEmpty(); }

        public Object[] toArray() { return new Object[0]; }

        public <T> T[] toArray(T[] a) {
            if (a.length > 0)
                a[0] = null;
            return a;
        }
        ... // 이하 오버라이딩 메서드 생략
    }
    ... // Collections 클래스 내 다른 필드나 메서드 생략
}      

 

자바 8 / 자바 9 이후

 

자바 8에서는 인터페이스가 정적 메서드를 가질 수 없는 제한이 풀렸다.


따라서 인스턴스화 불가 동반 클래스의 필요성이 없어졌고, 상단의 Collections 클래스 내부에 존재하는 public static 멤버들 상당수를 Collection 인터페이스에 둬도 상관없어졌다.

 

다만, 자바 8에서 인터페이스에는 public static 멤버만 허용하므로 여전히 많은 부분은 별도의 package-private 클래스에 두어야 한다.

 

Java 8에 추가된 Stream 인터페이스는 유용한 정적 메서드들을 제공한다.

 

Stream<String> alphabets = Stream.of("A", "B", "C");
Stream<String> empty = Stream.empty()

 

자바 9에서는 private static 메서드까지 인터페이스에 작성하는 것을 허락하지만, 정적 필드와 정적 멤버 클래스는 여전히 public 이어야 한다.

 

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

 

EnumSet 클래스는 new 연산자 사용이 불가능하다.


오직 정적 팩터리 메서드만으로 구현 객체를 반환 받을 수 있다.

 

// Food Enum 클래스를 다루는 비어있는 Set을 반환
EnumSet enumSet1 = EnumSet.noneOf(Food.class);

// 모든 Food Enum의 값을 담고있는 Set을 반환
EnumSet enumSet2 = EnumSet.allOf(Food.class);
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable {

       ... // 생략

       // 원소가 64개 이하면 RegularEnumSet의 인스턴스, 65개 이상이면 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);
    }
       ... // 생략
}  

 

클라이언트는 EnumSet을 활용할 때, RegularEnumSet과 JumboEnumSet 클래스의 존재를 모른다.


그렇기에 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려있어야 하고, 수정에 대해서는 닫혀있어야 한다.라는 OCP 원칙을 지킬 수 있습니다.

 

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

 

인터페이스나 클래스가 만들어지는 시점에는 하위 타입의 클래스가 존재하지 않아도 나중에 만들 클래스가 기존의 인터페이스나 클래스를 상속 받으면 언제든지 의존성을 주입 받아서 사용이 가능하다.

 

아래의 예제 코드와 같이 Student는 인터페이스이고 구현체가 없지만 School 클래스 내의 getStudents() 메소드는 작성 가능하다

 

public interface Student { ... }

public class School {
  public static List<Student> getStudents() {
    return new ArrayList<>();
  }
}

 

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

 

보통 정적 팩터리 메서드를 사용하는 경우 생성자를 private으로 제한하기 때문에 일어나는 문제이다.
다만, 아이템 18에서 다루게 될 상속보다 컴포지션을 사용하라라는 격언을 오히려 지킬 수 있도록 해준다는 역설이 존재한다.

 

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

 

반드시 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓도록 하자.

 

반응형