6 분 소요

이펙티브 자바를 읽고 공부하면서 정리를 해봤다.

오늘은 1장 내용이다.

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

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

클래스는 클라이언트에 public 생성자 대신 정적 팩토리 메서드를 제공 가능하다.

이게 생성자 보다 좋은 점이 뭐가 있을까??

1. 이름을 가질 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Item {  
  
    private String name;  
    private int price;  
    private int quantity;  
  
    public Item(String name, int price) {  
        this.name = name;  
        this.price = price;  
    }  
  
    public Item makeItemWithOutQuantity(String name, int price) {  
        return new Item(name, price);  
    }  
}

둘 다 같은 기능이지만 아래쪽이 "수량 없이 아이템 객체를 만듬"을 반환한다는 의미를 좀 더 잘 설명함.

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

인스턴스 재사용

정적 팩토리 메서드는 동일한 인스턴스를 여러 번 반환할 수 있다.

예를 들어, 불변 객체(ex. enum)나 상태가 없는 객체는 여러 곳에서 재사용 가능 → 메모리 절약

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Boolean {
    private final boolean value;

    private Boolean(boolean value) {
        this.value = value;
    }

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

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

이렇게 스태틱으로 만들어 놓은 덕분에 Boolean.valueOf()는 항상 TRUE/FALSE를 새로 생성하지 않고 반환함.

캐싱을 통한 성능 향상

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyClass {  
  
    private static final Map<String, MyClass> instances = new HashMap<>();  
  
    private String value;  
  
    private MyClass(String value) {  
        this.value = value;  
    }  
  
    public static MyClass getInstance(String value) {  
        if (!instances.containsKey(value))  
            instances.put(value, new MyClass(value));  
  
        return instances.get(value);  
    }  
  
    public String getValue() {  
        return value;  
    }  
}

다음과 같은 클래스가 있다.

1
private static final Map<String, MyClass> instances = new HashMap<>();

key에 대한 MyClass 인스턴스를 Map 자료구조로 저장해 놓는다.

1
2
3
4
5
6
public static MyClass getInstance(String value) {  
	if (!instances.containsKey(value))  
		instances.put(value, new MyClass(value));  

	return instances.get(value);  
}

키를 받아서 Map 자료구조에 없는 인스턴스라면 새로 인스턴스를 생성하고,

아니라면 있는 인스턴스를 리턴해 준다.

자 이러면 다음과 같이 재활용을 할 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClassTest {  
  
    @Test  
    void instanceTest() {  
        MyClass instance1 = MyClass.getInstance("key1");  
        System.out.println("instance1.getValue() = " + instance1.getValue());  
  
        MyClass instance2 = MyClass.getInstance("key1");  
        System.out.println("instance2.getValue() = " + instance2.getValue());  
  
        System.out.println(instance1 == instance2);  
  
        MyClass instance3 = MyClass.getInstance("key2");  
        System.out.println("instance3.getValue() = " + instance3.getValue());  
  
        System.out.println(instance1 == instance3);  
    }  
}

instance1과 2는 같은 key를 가지기 때문에 2를 만들 때는 새로운 객체를 생성하지 않을 것이다.

그리고 3는 새로운 key이기 때문에 새로운 객체를 만들 것이다.

결과를 보면 instance1 == instance2 인 걸 확인할 수 있다.

이렇게 정적 팩토리 메서드를 사용하면 객체 생성 비용을 절감하고, 동일한 인스턴스를 반복 사용함으로써 성능을 최적화 할 수 있다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface Fruit {  
    String getName();  
}  
  
class Apple implements Fruit {  
    public String getName() {  
        return "Apple";  
    }  
}  
  
class Orange implements Fruit {  
    public String getName() {  
        return "Orange";  
    }  
}  
  
class FruitFactory {  
  
    public static Fruit createFruit(String type) {  
        if (type.equalsIgnoreCase("Apple")) {  
            return new Apple();  
        } else if (type.equalsIgnoreCase("Orange")) {  
            return new Orange();  
        }  
        throw new IllegalArgumentException("알수 없는 타입의 과일입니다.");  
    }  
}

정적 팩토리 메서드는 반환 타입의 하위 타입 객체를 반환할 수 있다고 한다.

과일 인터페이스를 만들고, 과일을 상속 받는 여러 과일들을 만들었다.

그리고 정적 팩토리 메서드를 위한 클래스 FruitFactory을 만들었다.

createFruit()Fruit를 반환하는데 받아오는 값에 따라 다른 과일 즉 하위 타입 객체를 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FruitTest {  
  
    @Test  
    void createAppleTest() {  
        Fruit fruit = FruitFactory.createFruit("apple");  
        assertThat(fruit).isInstanceOf(Apple.class)  
                .extracting(Fruit::getName)  
                .isEqualTo("Apple");  
    }  
  
    @Test  
    void createOrangeTest() {  
        Fruit fruit = FruitFactory.createFruit("orange");  
        assertThat(fruit).isInstanceOf(Orange.class)  
                .extracting(Fruit::getName)  
                .isEqualTo("Orange");  
    }  
  
    @Test  
    void createBananaTest() {  
        assertThatThrownBy(() -> FruitFactory.createFruit("banana"))  
                .isInstanceOf(IllegalArgumentException.class)  
                .hasMessageContaining("알수 없는 타입의 과일입니다.");  
    }    
}

이렇게 테스트 코드를 작성했다.

1
Fruit fruit = FruitFactory.createFruit("apple");

이렇게 만들어 놓으면 하위 타입의 Apple 클래스를 반환 받을 수 있다.

즉 유연성이 높아진다.

정확히 Apple 인스턴스를 반환 한다.

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

정적 팩토리 메서드이기 때문에 파라미터를 받을 수 있고, 그렇기 때문에 반환 타입의 하위 타입이기만 하면 다른 객체를 반환 시킬 수 있다.

3번과 이어지는 장점이다.

EnumSet을 사용할 때 내부에는

1
2
3
4
5
6
7
8
9
10
11
12
13
EnumSet(Class<E> elementType, Enum<?>[] universe) {  
    this.elementType = elementType;  
    this.universe = universe;  
}  
  
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");  
    } else {  
        return (EnumSet)(universe.length <= 64 ? new RegularEnumSet(elementType, universe) : new JumboEnumSet(elementType, universe));  
    }  
}

다음과 같이 원소가 64개가 넘어가면 JumboEnumSet, 아니라면 RegularEnumSet을 사용하는데,

클라이언트 입장에서 알아야 할까? 그럴 필요가 없는 것이다.

그냥 EnumSet이라 선언만 해두면 알아서 분기 처리 되어 그 하위 객체를 반환 받을 수 있는 것이다.

캡슐화할 수 있게 된 것이다.

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

자 앞에 3, 4번에서 다음과 같이 설계하면 유연성이 생긴다고 했다.

이러한 유연함은 기능이 추가되거나 할 때 빛을 바란다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Mango implements Fruit {  
    public String getName() {  
        return "Mango";  
    }  
}  
  
class FruitFactory {  
  
    public static Fruit createFruit(String type) {  
        if (type.equalsIgnoreCase("Apple")) {  
            return new Apple();  
        } else if (type.equalsIgnoreCase("Orange")) {  
            return new Orange();  
        } else if (type.equalsIgnoreCase("Mango")) {  
            return new Mango();  
        }  
        throw new IllegalArgumentException("알수 없는 타입의 과일입니다.");  
    }  
}

아까 3번에 과일 팩토리에 망고를 추가하면 FruitFactory 만 건들 여서 망고 클래스를 추가할 수 있게 되는 것이다.

그럼 다른 코드에 영향을 최소화 하고, 기능을 추가한 것이다.

망고를 만들었고 테스트 해도, 기존 오렌지, 사과 테스트는 잘 통과가 된다.

단점 1. 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

어떻게 보면 장점인데,

다음과 같이 private으로 생성자를 만들어 놓고, 정적 팩터리 메서드만 제공하면

새로운 SubCar를 만드는데 어려움이 있다.

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

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

생성자는 자바 프로그래밍 언어의 스펙이므로 API 설명에 명확하게 나타나게 되지만,

정적 팩터리 메서드는 JavaDoc이 알아서 문서화 해줄 수 없다. API 문서를 잘 작성하고, 메서드 이름을 작성할 때도 규칙에 맞게 작성하는 것이 바람직 하다.

흔히 사용하는 정적 팩터리 메서드 명명 방식

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형 변환 메서드

  • of : 여러 매개변수를 받아서 적합한 타입의 인스턴스를 반환하는 집계 메서드

  • valueOf : fromof에 더 자세한 버전

  • instance 혹은 getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.

  • create 혹은 newInstance : instance와 같지만 매번 새로운 인스턴스를 생성해 반환함을 보장한다.

  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에서 팩터리 메서드를 정의할 때 사용. Type은 팩터리 메서드가 반환할 객체의 타입이다.

  • newType : newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에서 팩터리 메서드를 정의할 때 사용. Type은 팩터리 메서드가 반환할 객체의 타입이다.

  • Type : getType, newType의 간결한 버전

정리

정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.

하지만 정적 팩터리를 사용하는 게 유리한 경우가 더 많다.

태그:

카테고리:

업데이트:

댓글남기기