-
장점 1. 이름을 가질 수 있다.
-
장점 2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
-
장점 3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다 && 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
-
단점 1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
-
단점 2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
-
1. from
-
2. of
-
3. valueOf
-
4. instance 혹은 getInstance
-
5. create 혹은 newInstance
-
6. getType
-
7. newType
-
8. type
전통적으로 클라이언트가 클래스의 인스턴스를 얻는 방법은 public 생성자이다. 하지만, 클래스는 생성자와 별도로 정적 팩토리 메서드(static factory method)를 제공할 수 있다.
정적 팩토리 메서드는 네이밍에서 알 수 있듯이 정적인 팩토리 메서드이다. 그렇다면 팩토리란 무엇일까?
GoF 디자인 패턴 중 팩토리 메서드 패턴이 존재한다. 이는 인스턴스 생성을 위한 프레임워크와 실제 인스턴스를 생성하는 클래스를 분리할 때 사용하는 패턴이다.
여기서 유래가 되어 팩토리는 객체 생성의 역할을 분리하겠다는 의미가 담겨있다.
정리하자면 정적 팩토리 메서드는 객체 생성이란 관심사를 갖는 정적 메서드라고 해석할 수 있다.
글 제목에서 알 수 있듯이 생성자보다 정적 팩토리 메서드가 갖는 장점이 많다. 한 번 알아보자!
장점 1. 이름을 가질 수 있다.
생성자는 기본적으로 이름을 가질 수 없고, 클래스의 이름과 동일하다.
다음은 바코드 번호와 상품 이름을 매개변수로 받아 상품 객체를 생성하는 클래스이다.
public class Product {
private static final long APPLE_BAR_CORD_NUMBER = 123456789L;
private static final long BANANA_BAR_CORD_NUMBER = 9876654321L;
private static final String APPLE = "apple";
private static final String BANANA = "banana";
private String name; // 상품 이름
private long barCordNumber; // 상품 바코드 번호
public Product(String name, long barCordNumber) {
this.name = name;
this.barCordNumber = barCordNumber;
}
public Product(long barCordNumber) {
this.barCordNumber = barCordNumber;
if (barCordNumber == APPLE_BAR_CORD_NUMBER) {
this.name = APPLE;
}
if (barCordNumber == BANANA_BAR_CORD_NUMBER) {
this.name = BANANA;
}
}
public Product(String name) {
this.name = name;
if (name == APPLE) {
this.barCordNumber = APPLE_BAR_CORD_NUMBER;
}
if (name == BANANA) {
this.barCordNumber = BANANA_BAR_CORD_NUMBER;
}
}
}
이는 생성자를 오버로딩한 것으로, 전달받는 매개변수에 따라 구현이 다르다.
Product apple = new Product(123456789L);
Product banana = new Product("banana");
위와 같이 객체를 생성하게 되면, 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 알 수 없다. 또, 생성자가 어떤 역할을 하는지 파악하기 어려워 엉뚱한 것을 호출할 수도 있다.
반대로 정적 팩토리 메서드는 이름을 통해 반환될 객체의 특성을 묘사하기 쉽다.
public class ProductFactory {
private static final long APPLE_BAR_CORD_NUMBER = 123456789L;
private static final long BANANA_BAR_CORD_NUMBER = 9876654321L;
private static final String APPLE = "apple";
private static final String BANANA = "banana";
public static Product createByName(String name) {
if (name == APPLE) {
return new Product(APPLE, APPLE_BAR_CORD_NUMBER);
}
if (name == BANANA) {
return new Product(BANANA, BANANA_BAR_CORD_NUMBER);
}
throw new NoSuchElementException("상품이 존재하지 않습니다.");
}
public static Product createByBarCordNumber(long barCordNumber) {
if (barCordNumber == APPLE_BAR_CORD_NUMBER) {
return new Product(APPLE, APPLE_BAR_CORD_NUMBER);
}
if (barCordNumber == BANANA_BAR_CORD_NUMBER) {
return new Product(BANANA, BANANA_BAR_CORD_NUMBER);
}
throw new NoSuchElementException("상품이 존재하지 않습니다.");
}
}
Product apple = ProductFactory.createByBarCordNumber(123456789L);
Product banana = ProductFactory.createByName("banana");
위와 같이 정적 팩토리 메서드를 사용하여 객체를 사용하면 바코드 번호를 사용하여 상품을 생성하는지, 이름을 사용하여 상품을 생성하는지 한눈에 알아볼 수 있을 것이다.
이렇듯 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것이라 판단되면, 생성자를 정적 팩토리 메서드로 바꾸고, 그들의 차이를 잘 드러내도록 네이밍 하자.
장점 2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 불필요한 객체 생성을 피할 수 있다.
public class ProductQuantity {
private static final int MIN_PRODUCT_QUANTITY = 1;
private static final int MAX_PRODUCT_QUANTITY = 100;
private static Map<Integer, ProductQuantity> productQuantityCache = new HashMap<>();
static {
IntStream.range(MIN_PRODUCT_QUANTITY, MAX_PRODUCT_QUANTITY)
.forEach(i -> productQuantityCache.put(i, new ProductQuantity(i)));
}
private int quantity;
private ProductQuantity(int quantity) {
this.quantity = quantity;
}
public ProductQuantity of(int quantity){
return productQuantityCache.get(quantity);
}
}
위처럼 상품의 수량이 1개부터 100개까지 존재할 수 있다. 상품 수량을 enum으로 만들 수 있지만, ProductQuantity 클래스 안에서 반복문을 통해 100개의 인스턴스를 만들 수도 있다.
이렇게 미리 상품 수량 객체를 캐싱을 통해 새로운 객체 생성의 부담을 줄일 수 있는 장점이 존재한다.
또, 생성자의 접근 제한자를 private로 설정하여 상품 수량 객체의 생성을 정적 팩토리 메서드로만 가능하도록 제한할 수 있다.
이런 클래스를 인스턴스 통제(instance-controlled) 클래스라고 한다. 인스턴스 통제 클래스를 통해 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다.
인스턴스를 통제하는 이유
- 클래스를 싱글턴(singleton)으로 만들 수 있다.
- 인스턴스화 불가로 만들 수 있다.
- 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다. ( a == b일 때만 a.equals(b) 성립 )
이를 통해 정해진 상품 수량의 범위에 벗어나는 상품 수량의 생성을 막을 수 있다는 장점도 있다.
장점 3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다 && 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
이를 통해 반환할 객체의 클래스를 자유롭게 선택할 수 있다.
ProductLevel이라는 상위 클래스와 이를 상속받는 Basic, Premium이라는 클래스가 존재한다고 가정하자.
상품의 가격에 따라 다른 상품 등급을 반환하도록 하는 정적 팩토리 메서드를 통해 하위 타입의 객체를 반환할 수 있다.
public class ProductLevelFactory {
public static ProductLevelFactory of(int price) {
if (price < 1000000) {
return new Basic();
} else {
return new Premium();
}
}
}
상품의 가격이 백만 원 미만이라면 그 상품은 Basic 등급, 백만 원 이상이라면 Premium 등급이라고 가정하자. 클라이언트는 이 두 클래스의 존재를 몰라도 된다.
팩토리가 반환하는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없다. 그저 ProductLevelFactory이기만 하면 된다.
장점이 있다면 단점도 있기 마련이다. 정적 팩토리 메서드의 단점을 알아보자!
단점 1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
인스턴스 통제 클래스를 사용하기 위해서는 사용자가 new를 통해 객체를 생성하는 것을 막아야 한다. 그러기 위해서는 생성자의 접근 제한자를 private나 protected로 설정해야 하는데, 이렇게 되면 상속을 할 수 없게 된다.
이 단점은 다른 시선으로 보면 상속보다 컴포지션을 사용하도록 하고, 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수 있다.
단점 2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
자바독(JavaDoc)에서 클래스의 생성자는 명확히 드러나지만, 정적 팩토리 메서드는 일반 메서드이기 때문에 프로그래머가 직접 문서를 찾아야 한다.
https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html
javadoc
One use page for each package (package-use.html) and a separate use page for each class and interface (class-use/classname.html). The use page describes what packages, classes, methods, constructors and fields use any part of the specified class, interface
docs.oracle.com
그렇기 때문에 메서드 이름을 짓는 규칙을 통해 문제를 완화해줘야 한다.
그렇다면 마지막으로 정적 팩토리 메서드 네이밍에 흔히 사용하는 방식을 알아보자.
1. from
매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형 변환 메서드
Date d = Date.from(instant);
2. of
여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
3. valueOf
from과 of의 더 자세한 버전
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
4. instance 혹은 getInstance
(매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
StackWalker luke = StackWalker.getInstance(options);
5. create 혹은 newInstance
instance 혹은 getInstance와 비슷하지만, 매번 새로운 인스턴스를 생성하여 반환함을 보장한다.
Object newArray = Array.newInstance(classObject, arrayLen);
6. getType
getInstance 와 같으나, 현재 클래스가 아닌 다른 클래스의 인스턴스를 생성할 때 사용한다. Type 은 팩토리 메서드가 반환할 객체의 타입을 적는다.
FileStore fs = Files.getFileStore(path);
7. newType
createInstance와 같으나, 현재 클래스가 아닌 다른 클래스의 인스턴스를 생성할 때 사용한다. Type 은 팩토리 메서드가 반환할 객체의 타입을 적는다.
BufferedReader br = Files.newBufferedReader(path);
8. type
getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);
*이 포스팅은 제가 "이펙티브 자바 3/E"를 읽고 공부한 내용을 정리한 것입니다. 내용에 문제가 있으면 알려주시면 감사하겠습니다! *
전통적으로 클라이언트가 클래스의 인스턴스를 얻는 방법은 public 생성자이다. 하지만, 클래스는 생성자와 별도로 정적 팩토리 메서드(static factory method)를 제공할 수 있다.
정적 팩토리 메서드는 네이밍에서 알 수 있듯이 정적인 팩토리 메서드이다. 그렇다면 팩토리란 무엇일까?
GoF 디자인 패턴 중 팩토리 메서드 패턴이 존재한다. 이는 인스턴스 생성을 위한 프레임워크와 실제 인스턴스를 생성하는 클래스를 분리할 때 사용하는 패턴이다.
여기서 유래가 되어 팩토리는 객체 생성의 역할을 분리하겠다는 의미가 담겨있다.
정리하자면 정적 팩토리 메서드는 객체 생성이란 관심사를 갖는 정적 메서드라고 해석할 수 있다.
글 제목에서 알 수 있듯이 생성자보다 정적 팩토리 메서드가 갖는 장점이 많다. 한 번 알아보자!
장점 1. 이름을 가질 수 있다.
생성자는 기본적으로 이름을 가질 수 없고, 클래스의 이름과 동일하다.
다음은 바코드 번호와 상품 이름을 매개변수로 받아 상품 객체를 생성하는 클래스이다.
public class Product {
private static final long APPLE_BAR_CORD_NUMBER = 123456789L;
private static final long BANANA_BAR_CORD_NUMBER = 9876654321L;
private static final String APPLE = "apple";
private static final String BANANA = "banana";
private String name; // 상품 이름
private long barCordNumber; // 상품 바코드 번호
public Product(String name, long barCordNumber) {
this.name = name;
this.barCordNumber = barCordNumber;
}
public Product(long barCordNumber) {
this.barCordNumber = barCordNumber;
if (barCordNumber == APPLE_BAR_CORD_NUMBER) {
this.name = APPLE;
}
if (barCordNumber == BANANA_BAR_CORD_NUMBER) {
this.name = BANANA;
}
}
public Product(String name) {
this.name = name;
if (name == APPLE) {
this.barCordNumber = APPLE_BAR_CORD_NUMBER;
}
if (name == BANANA) {
this.barCordNumber = BANANA_BAR_CORD_NUMBER;
}
}
}
이는 생성자를 오버로딩한 것으로, 전달받는 매개변수에 따라 구현이 다르다.
Product apple = new Product(123456789L);
Product banana = new Product("banana");
위와 같이 객체를 생성하게 되면, 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 알 수 없다. 또, 생성자가 어떤 역할을 하는지 파악하기 어려워 엉뚱한 것을 호출할 수도 있다.
반대로 정적 팩토리 메서드는 이름을 통해 반환될 객체의 특성을 묘사하기 쉽다.
public class ProductFactory {
private static final long APPLE_BAR_CORD_NUMBER = 123456789L;
private static final long BANANA_BAR_CORD_NUMBER = 9876654321L;
private static final String APPLE = "apple";
private static final String BANANA = "banana";
public static Product createByName(String name) {
if (name == APPLE) {
return new Product(APPLE, APPLE_BAR_CORD_NUMBER);
}
if (name == BANANA) {
return new Product(BANANA, BANANA_BAR_CORD_NUMBER);
}
throw new NoSuchElementException("상품이 존재하지 않습니다.");
}
public static Product createByBarCordNumber(long barCordNumber) {
if (barCordNumber == APPLE_BAR_CORD_NUMBER) {
return new Product(APPLE, APPLE_BAR_CORD_NUMBER);
}
if (barCordNumber == BANANA_BAR_CORD_NUMBER) {
return new Product(BANANA, BANANA_BAR_CORD_NUMBER);
}
throw new NoSuchElementException("상품이 존재하지 않습니다.");
}
}
Product apple = ProductFactory.createByBarCordNumber(123456789L);
Product banana = ProductFactory.createByName("banana");
위와 같이 정적 팩토리 메서드를 사용하여 객체를 사용하면 바코드 번호를 사용하여 상품을 생성하는지, 이름을 사용하여 상품을 생성하는지 한눈에 알아볼 수 있을 것이다.
이렇듯 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것이라 판단되면, 생성자를 정적 팩토리 메서드로 바꾸고, 그들의 차이를 잘 드러내도록 네이밍 하자.
장점 2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 불필요한 객체 생성을 피할 수 있다.
public class ProductQuantity {
private static final int MIN_PRODUCT_QUANTITY = 1;
private static final int MAX_PRODUCT_QUANTITY = 100;
private static Map<Integer, ProductQuantity> productQuantityCache = new HashMap<>();
static {
IntStream.range(MIN_PRODUCT_QUANTITY, MAX_PRODUCT_QUANTITY)
.forEach(i -> productQuantityCache.put(i, new ProductQuantity(i)));
}
private int quantity;
private ProductQuantity(int quantity) {
this.quantity = quantity;
}
public ProductQuantity of(int quantity){
return productQuantityCache.get(quantity);
}
}
위처럼 상품의 수량이 1개부터 100개까지 존재할 수 있다. 상품 수량을 enum으로 만들 수 있지만, ProductQuantity 클래스 안에서 반복문을 통해 100개의 인스턴스를 만들 수도 있다.
이렇게 미리 상품 수량 객체를 캐싱을 통해 새로운 객체 생성의 부담을 줄일 수 있는 장점이 존재한다.
또, 생성자의 접근 제한자를 private로 설정하여 상품 수량 객체의 생성을 정적 팩토리 메서드로만 가능하도록 제한할 수 있다.
이런 클래스를 인스턴스 통제(instance-controlled) 클래스라고 한다. 인스턴스 통제 클래스를 통해 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다.
인스턴스를 통제하는 이유
- 클래스를 싱글턴(singleton)으로 만들 수 있다.
- 인스턴스화 불가로 만들 수 있다.
- 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다. ( a == b일 때만 a.equals(b) 성립 )
이를 통해 정해진 상품 수량의 범위에 벗어나는 상품 수량의 생성을 막을 수 있다는 장점도 있다.
장점 3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다 && 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
이를 통해 반환할 객체의 클래스를 자유롭게 선택할 수 있다.
ProductLevel이라는 상위 클래스와 이를 상속받는 Basic, Premium이라는 클래스가 존재한다고 가정하자.
상품의 가격에 따라 다른 상품 등급을 반환하도록 하는 정적 팩토리 메서드를 통해 하위 타입의 객체를 반환할 수 있다.
public class ProductLevelFactory {
public static ProductLevelFactory of(int price) {
if (price < 1000000) {
return new Basic();
} else {
return new Premium();
}
}
}
상품의 가격이 백만 원 미만이라면 그 상품은 Basic 등급, 백만 원 이상이라면 Premium 등급이라고 가정하자. 클라이언트는 이 두 클래스의 존재를 몰라도 된다.
팩토리가 반환하는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없다. 그저 ProductLevelFactory이기만 하면 된다.
장점이 있다면 단점도 있기 마련이다. 정적 팩토리 메서드의 단점을 알아보자!
단점 1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
인스턴스 통제 클래스를 사용하기 위해서는 사용자가 new를 통해 객체를 생성하는 것을 막아야 한다. 그러기 위해서는 생성자의 접근 제한자를 private나 protected로 설정해야 하는데, 이렇게 되면 상속을 할 수 없게 된다.
이 단점은 다른 시선으로 보면 상속보다 컴포지션을 사용하도록 하고, 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수 있다.
단점 2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
자바독(JavaDoc)에서 클래스의 생성자는 명확히 드러나지만, 정적 팩토리 메서드는 일반 메서드이기 때문에 프로그래머가 직접 문서를 찾아야 한다.
https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html
javadoc
One use page for each package (package-use.html) and a separate use page for each class and interface (class-use/classname.html). The use page describes what packages, classes, methods, constructors and fields use any part of the specified class, interface
docs.oracle.com
그렇기 때문에 메서드 이름을 짓는 규칙을 통해 문제를 완화해줘야 한다.
그렇다면 마지막으로 정적 팩토리 메서드 네이밍에 흔히 사용하는 방식을 알아보자.
1. from
매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형 변환 메서드
Date d = Date.from(instant);
2. of
여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
3. valueOf
from과 of의 더 자세한 버전
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
4. instance 혹은 getInstance
(매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
StackWalker luke = StackWalker.getInstance(options);
5. create 혹은 newInstance
instance 혹은 getInstance와 비슷하지만, 매번 새로운 인스턴스를 생성하여 반환함을 보장한다.
Object newArray = Array.newInstance(classObject, arrayLen);
6. getType
getInstance 와 같으나, 현재 클래스가 아닌 다른 클래스의 인스턴스를 생성할 때 사용한다. Type 은 팩토리 메서드가 반환할 객체의 타입을 적는다.
FileStore fs = Files.getFileStore(path);
7. newType
createInstance와 같으나, 현재 클래스가 아닌 다른 클래스의 인스턴스를 생성할 때 사용한다. Type 은 팩토리 메서드가 반환할 객체의 타입을 적는다.
BufferedReader br = Files.newBufferedReader(path);
8. type
getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);
*이 포스팅은 제가 "이펙티브 자바 3/E"를 읽고 공부한 내용을 정리한 것입니다. 내용에 문제가 있으면 알려주시면 감사하겠습니다! *