이펙티브 자바 1 - 생성자 대신 정적 팩터리 메서드를 고려하라
이펙티브 자바를 읽고 공부하면서 정리를 해봤다.
오늘은 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:from과of에 더 자세한 버전 -
instance혹은getInstance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다. -
create혹은newInstance:instance와 같지만 매번 새로운 인스턴스를 생성해 반환함을 보장한다. -
getType:getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에서 팩터리 메서드를 정의할 때 사용.Type은 팩터리 메서드가 반환할 객체의 타입이다. -
newType:newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에서 팩터리 메서드를 정의할 때 사용.Type은 팩터리 메서드가 반환할 객체의 타입이다. -
Type:getType,newType의 간결한 버전
정리
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
하지만 정적 팩터리를 사용하는 게 유리한 경우가 더 많다.
댓글남기기