Background
Clean Code 책을 읽는 도중 정적 팩토리 메서드에 대한 내용이 나오면서 그냥 넘어갈 수가 없어서 따로 찾아보기로 했다.
우선 간단하게 팩토리 메서드 먼저 알아보고 정적 팩토리 메서드를 알아보자.
참고로 factory method pattern과 static factory method는 관련이 없다.
Factory method pattern
Factory method 란?
Factory method pattern은 생성될 객체의 정확한 클래스를 지정하지 않고도 객체 생성 문제를 처리하기 위해 factory method를 사용하는 생성 패턴이다.
생성 패턴(Creational pattern)
인스턴스를 만드는 절차를 추상화하는 패턴이다.
이 범주에 해당하는 패턴은 객체를 생성 합성하는 방법이나 객체의 표현 방법과 S/W 시스템을 분리해 준다.
생성자를 호출하는 대신 인터페이스에 지정되고, 하위 클래스에 의해 구현되거나 기본 클래스에 구현되고 선택적으로 파생 클래스에 의해 재정의되는 factory method를 호출하여 객체를 생성함으로써 수행된다.
즉, factory method 디자인 패턴은 유연하고 재사용 가능한 객체지향 소프트웨어이다.
Factory method 정의
GoF(Gang of Four)
객체 생성을 위한 인터페이스를 정의하되 인스턴스화할 클래스를 하위 클래스에서 결정하도록 한다.
Factory method를 사용하면 클래스가 사용하는 인스턴스화를 하위 클래스로 연기할 수 있다.
객체를 생성하려면 구성 객체에 포함하기에는 적합하지 않은 복잡한 프로세스가 필요한 경우가 많다. 객체 생성으로 인해 상당한 코드 중복이 발생할 수 있고, 구성 객체에 액세스 할 수 없는 정보가 필요할 수 있으며, 충분한 수준의 추상화를 제공하지 못하거나, 구성 객체의 관심사가 아닐 수도 있다.
하지만 factory method 디자인 패턴은 객체를 생성하기 위한 별도의 메서드를 정의함으로써 이러한 문제를 처리한다.
Factory method 패턴은 상속에 의존한다.
객체 생성은 객체를 생성하기 위해 factory method를 구현하는 하위 클래스에 위임되기 때문이다.
Factory method 구조
1. Product는 인터페이스를 선언한다.
제품
Factory method에서 반환된 객체를 종종 제품이라고 부른다.
인터페이스
생성자와 자식 클래스들이 생성할 수 있는 모든 객체의 공통이다.
2. Concreate Product는 제품 인터페이스의 다양한 구현들이다.
3. Creator 클래스는 새로운 제품 객체들을 반환하는 factory method를 선언한다.
중요한 점은 이 factory method의 반환 유형이 제품 인터페이스와 일치해야 한다는 것이다.
Creator는 이름처럼 제품을 생성하는 것이 아니다. 일반적으로 creator 클래스에는 이미 product와 관련된 핵심 비즈니스 로직이 있으며, factory method는 이 로직을 concreate product 클래스들로부터 분리하는 데 도움을 준다.
4. Concreate Creator들은 기초 factory method를 오버라이드(재정의)하여 다른 유형의 제품을 반환하게 하도록 한다.
Factory method는 기존 객체들을 캐시, 객체 풀 또는 다른 소스로부터 반환할 수 있다. 즉, factory method는 항상 새로운 인스턴스들을 생성해야 할 필요가 없다.
Static factory method
Static factory method는 객체의 생성을 담당하는 클래스 메서드로 생성자를 통해서가 아닌 static method를 통해서 객체를 생성하는 역할을 한다. 여기서 factory는 객체를 생성하는 역할을 분리한 것을 의미한다.
위에서도 말했지만 static factory method는 factory method pattern과는 관련이 없다.
Effective Java의 저자인 조슈아는 static factory method가 생성자를 대체할 만큼의 장점을 가지고 있으나, 단점도 있으니 생성자를 만들 때 고려해 보라고 했다.
장점
1. 이름을 가질 수 있어 가독성이 좋다.
public class Order {
private boolean car;
private boolean ship;
private Product product;
public Order(Product product, boolean car) {
this.product = product;
this.car = car;
}
/* Order라는 메서드가 (Product, boolean) 타입으로 중복되어서 에러 발생
public Order(Product product, boolean ship) {
this.product = product;
this.ship = ship;
}
*/
public Order(boolean ship, Product product) {
this.product = product;
this.ship = ship;
}
}
생성자는 인자로 받는 파라미터의 개수에 따라 생성자를 여러 개 생성할 수 있지만, 클래스명을 동일하게 사용해야 하는 규칙 때문에 각각의 생성자는 이름을 가질 수 없다.
따라서 생성자를 사용하면 객체를 생성하려는 것이 car order인지 ship order인지 알 수 없다.
이를 해결하기 위해 static factory method를 사용한다.
생성자의 파라미터 타입이 중복되는 경우에 사용하면 좋다.
public class Order {
private boolean car;
private boolean ship;
private Product product;
public Order() {
}
public static Order carOrder(Product product) {
Order order = new Order();
order.product = product;
order.car = true;
return order;
}
public static Order shipOrder(Product product) {
Order order = new Order();
order.product = product;
order.ship = true;
return order;
}
}
2. 호출할 때마다 인스턴스를 새로 생성하지 않아도 된다.
불변 클래스(immutable class)는 인스턴스를 미리 생성해 두거나, 새로 생성한 인스턴스를 캐싱해서 재활용하기 때문에 불필요한 객체 생성을 줄일 수 있다.
Static factory method와 캐싱구조를 함께 사용하면 매번 새로운 객체를 만들 필요가 없다.
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // true
}
}
생성자를 private으로 제한해서 새로운 객체 생성을 제한하고, `getInstance()` 메서드를 static으로 선언해서 인스턴스를 생성하도록 한다.
위 코드의 싱글톤 객체 sington1과 singleton2는 같은 인스턴스이다.
대표적인 예는 Boolean Class로 `TRUE`, `FALSE`를 상수로 정의해서 `valueOf(boolean)` 메서드 사용 시 객체를 새로 생성하는 것이 아니라 상수를 반환하는 것이다.
public final class Boolean implements java.io.Serializable,
Comparable<Boolean>
{
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code true}.
*/
public static final Boolean TRUE = new Boolean(true);
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);
...
}
public sataic Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE:
}
따라서 객체 생성 비용이 큰 객체가 자주 요청이 된다면 성능상에서 이점을 볼 수 있게 되는 것이다.
이렇게 인스턴스를 통제하는 것은 인스턴스가 단 하나뿐임을 보장하는 것이고, Flyweight Pattern의 근간이 된다.
Flyweight Pattern
데이터를 공유해서 메모리를 절약하는 패턴으로, 공통으로 사용되는 객체는 한 번만 사용되고, Pool에 의해서 관리/사용되는 디자인 패턴이다.
예로는 JVM의 String Constant Pool이 있다.
3. 리턴 타입의 하위 타입 객체를 반환할 수 있다.
상속을 활용할 때 나타나는 특징이다.
Java의 다형성 특징을 이용하면 인터페이스 자체를 반환하도록 할 수 있다.
즉, 인터페이스만 공개하여 하위 클래스를 노출시키지 않고 반환할 수 있다.
interface Product {}
class Car implements Product {}
class Ship implements Product {}
class Products {
public static Product getCar() {
return new Car();
}
public static Product getShip() {
return new Ship();
}
}
위와 같은 코드는 인터페이스를 static factory method의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심이 된다.
하지만 java 8부터는 인터페이스가 정적 메서드를 가질 수 있게 되어 동반 클래스 개념은 더 이상 필요 없어졌다.
interface Product {
public static Product getCar() {
return new Car();
}
public static Product getShip() {
return new Ship();
}
}
즉, 위 코드처럼 인터페이스에 static factory method를 선언하면 된다.
대표적으로 java의 컬렉션 프레임워크인 java.util.Collections 클래스는 Collection 인터페이스를 반환하는 여러 static factory method를 가지고 있다.
public Collections(){
...
public static final List EMPTY_LIST = new EmptyList<>();
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
...
}
Collections 클래스와 Collection 인터페이스 이름에서 s를 붙인 것을 Collection 인터페이스의 동반 클래스(Companion Class)라고 부른다. (위의 Products는 컬렉션을 응용한 것이다.)
하지만 위에서 말했듯 java 8부터는 인터페이스에 static factory method를 선언하면 된다.
public interface List<E> extends Collection<E> {
...
static <E> List<E> of() {
return (List<E>) ImmutableCollections.ListN.EMPTY_LIST;
}
...
}
위 코드는 java 9 List 인터페이스의 `of()` 메서드로 인터페이스를 반환하는 static factory method이다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
메서드 내에서 분기처리를 통해 여러 자식 타입의 인스턴스를 반환하도록 응용할 수 있다.
interface Product {
public static Product getCar(int price) {
if(price > 100) {
return new kiaCar();
} else if(price > 10) {
return new hyundaiCar();
}
return new samsungCar();
}
}
(차 회사 순서는 아무 이유 없는 순서인 단순한 예시입니다ㅎ..)
5. 객체 생성을 캡슐화할 수 있다.
생성자를 사용하는 경우, 외부에 내부 구현을 드러내야 하는데, static factory method를 사용하면 내부 구현을 캡슐화하여 사용할 수 있다.
또한, 정보 은닉성이라는 특징도 가지는 동시에, 사용하고 있는 구현체를 숨겨 의존성을 제거해 주는 장점도 지니고 있다.
예시는 자주 사용하는 DTO와 Entity 간의 형변환이다.
@Builder
public class ProductDto {
private String name;
private String price;
public static ProductDto from(Product product) {
return new ProductDto(product.getName(), product.getDate());
}
}
public class ProductDtoTest {
public static void main(String[] args) {
...
Product productDto = new ProductDto(product.getName(), product.getDate()); // 생성자
Product productDto = ProductDto.from(product); // 정적 팩토리 메서드
}
}
위 코드처럼 static factory method를 사용하면 단순히 생성자의 역할을 하는 것뿐만 아니라, 가독성이 더 좋은 코드를 작성할 수 있고, 객체지향적으로 프로그래밍할 수 있도록 도와준다.
단점
1. 상속을 하려면 public이나 protected 생성자가 필요하다.
static factory method만 제공하면 하위 클래스를 만들 수 없다.
2. 개발자가 해당 메서드를 찾기 어렵다.
javadoc의 내용을 보면 생성자는 문서로 정리되어 있다.
하지만 static factory method는 따로 정리되지 않기 때문에 확인 작업이 필요하다.
클래스 내에 메서드가 많을 경우 static factory method 또한 문서에서 찾기 힘들다.
이 부분에 대한 해결책으로는 static factory method의 namaing을 신경 써서 지어야 쉽게 알 수 있다.
Naming Convention
- from
- 매개변수를 하나 받아 해당 타입의 인스턴스를 반환하는 형변환 메서드
- `Date date = Date.from(instant);`
- of
- 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
- `Set<Rank> colors = EnumSet.of(RED, BLUE, GREEN);`
- valueOf
- `from`과 `of`의 더 자세한 버전
- `BigInteger value = BigInteger.valueOf(Integer.MAX_VALUE);`
- instance, getInstance
- 매개변수를 받을 때 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는 메서드
- Sington Pattern에서 주로 사용
- `User user = User.getInstance(options);`
- create, newInstance
- `instance` 혹은 `getInstance`와 같지만 매번 새로운 인스턴스를 생성해서 반환하는 메서드
- `Object newArr = Array.newInstance(classObj, arrayLen);`
- getType
- `getInstance`와 같지만 생성할 클래스가 아닌 클래스의 factory method를 정의할 때 사용하는 메서드
- `FileStore fs = Files.getFileStore(path);`
- newType
- `newInstance`와 같지만 생성할 클래스가 아닌 클래스의 factory method를 정의할 때 사용하는 메서드
- `BufferedReader br = Files.newBufferedReader(path);`
- type
- `getType`과 `newType`의 간결한 버전
- `List<User> users = Collections.list(currentUsers);`
References
- Factory method pattern
- Static factory method