: 개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체를 생성하는 디자인 패턴이다.
객체를 인스턴스화할 때 직접적으로 생성자를 호출하여 생성하는 것이 아니라,
객체 생성의 역할을 하는 별도의 클래스 메서드를 통해 간접적으로 객체 생성을 유도하는 것이다.
class Book {
private String title;
// 생성자를 private화 하여 외부에서 생성자 호출 차단
private Book(String title) { this.title = title; }
// 정적 팩토리 메서드
public static Book titleOf(String title) {
return new Book(title); // 메서드에서 생성자를 호출하고 리턴함
}
}
public static void main(String[] args) {
// 정적 메서드 호출을 통해 인스턴스화된 객체를 얻음
Book book1 = Book.titleOf("어린왕자");
}
정적 팩토리 메서드는 GOF의 팩토리 메서드, 추상 팩토리 패턴의 팩토리(Factory) 개념을 따와 심플하게 변형시킨 팩토리 변형 패턴의 종류라고 보면 된다.
멀쩡한 생성자를 냅두고 번거롭게 한단계 거쳐 정적 팩토리 메서드를 통해 객체를 생성하는지에 대한 실용성에 대해 의문을 가질테지만,
정적 팩토리 메서드는 단순히 생성자의 역할을 대신 이행하는 것 뿐만 아니라 개발자가 좀 더 가독성 좋은 코드를 작성하고 객체 지향적으로 프로그래밍 할 수 있게 도와준다.
class Car {
private String brand;
private String color = "black";
public Car(String brand, String color) {
this.brand = brand;
this.color = color;
}
public Car(String brand) {
this.brand = brand;
}
}
public static void main(String[] args) {
// 검정색 테슬라 자동차
Car teslaCar = new Car("Tesla");
// 빨간색 BMW 자동차
Car bmwRedCar = new Car("BMW", "Red");
}
그리고, new 생성자 방법은 단지 매개변수의 유형과 개수를 제안할 뿐이지 어떠한 역할 표현이나 편의성을 제공하지 않는다는 면에서 생성자로 넘기는 매개변수 만으로는 반환될 객체의 특성을 제대로 표현하기가 어렵다는 것이다.
따라서 정적 메서드를 통해 적절한 메서드 네이밍을 해준다면 반환될 객체의 특성을 한번에 유추할 수 있게 된다. 왜냐하면 네이밍을 통해 어떤 값을 이용해 객체를 만들려고 하는지 쉽게 설계 의도를 전달할 수 있기 때문이다.
class Car {
private String brand;
private String color;
// private 생성자
private Car(String brand, String color) {
this.brand = brand;
this.color = color;
}
// 정적 팩토리 메서드 (매개변수 하나는 from 네이밍)
public static Car brandBlackFrom(String brand) {
return new Car(brand, "black");
}
// 정적 팩토리 메서드 (매개변수 여러개는 of 네이밍)
public static Car brandColorOf(String brand, String color) {
return new Car(brand, color);
}
}
public static void main(String[] args) {
// 검정색 테슬라 자동차
Car teslaCar = Car.brandBlackFrom("Tesla");
// 빨간색 BMW 자동차
Car bmwRedCar = Car.brandColorOf("BMW", "Red");
}
이처럼 생성자 대신 정적 팩토리 메서드를 호출함으로써 생성될 객체의 특성에 대해 쉽게 묘사할 수 있다는 장점이 있어 코드의 가독성을 높여주게 된다. 생성자로 만드는 것보다 의미를 가진 메소드를 이용하면 객체 생성의 의미를 훨씬 파악하기 쉽기 때문이다.
대표적인 예로 Singleton 디자인 패턴Visit Website 을 들 수 있는데, getInstance() 라는 정적 팩토리 메서드를 사용해 오로지 하나의 객체만 반환하도록 하여 객체를 재사용해 메모리를 아끼도록 유도할 수 있다.
class Singleton {
private static Singleton instance;
private Singleton() {}
// 정적 팩토리 메서드
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
또다른 예로는 인스턴스에 대한 캐싱(Caching) 절차 구조를 정적 팩토리 메서드로 구현할 수 있다. 인스턴스에 대해 캐싱을 한다면 필요한 인스턴스만 뽑아 재사용하여 메모리를 절약할 수 있게 된다.
인스턴스를 통제하는 것은 인스턴스가 단 하나뿐임을 보장하는 것이고, Flyweight 디자인 패턴Visit Website의 근간이 되게 된다.
class Day {
private String day;
public Day(String day) { this.day = day; }
public String getDay() { return day; }
}
// Day 객체를 생성하고 관리하는 Flyweight 팩토리 클래스
class DayFactory {
// Day 객체를 저장하는 캐싱 저장소 역할
private static final Map<String, Day> cache = new HashMap<>();
// 자주 사용될것 같은 Day 객체 몇가지를 미리 등록한다
static {
cache.put("Monday", new Day("Monday"));
cache.put("Tuesday", new Day("Tuesday"));
cache.put("Wednesday", new Day("Wednesday"));
}
// 정적 팩토리 메서드 (인스턴스에 대해 철저한 관리)
public static Day from(String day) {
if(cache.containsKey(day)) {
// 캐시 되어있으면 그대로 가져와 반환
System.out.println("해당 요일은 캐싱되어 있습니다.");
return cache.get(day);
} else {
// 캐시 되어 있지 않으면 새로 생성하고 캐싱하고 반환
System.out.println("해당 요일은 캐싱되어 있지 않아 새로 생성하였습니다.");
Day d = new Day(day);
cache.put(day, d);
return d;
}
}
}
이렇게 인스턴스의 생성에 관여하여, 생성되는 인스턴스의 수를 통제할 수 있는 클래스를 인스턴스 통제 (instance-controlled) 클래스라고 한다.
interface SmarPhone {
public static SmarPhone getSamsungPhone() {
return new Galaxy();
}
public static SmarPhone getApplePhone() {
return new IPhone();
}
public static SmarPhone getChinesePhone() {
return new Huawei();
}
}
interface SmarPhone {
public static SmarPhone getPhone(int price) {
if(price > 100000) {
return new IPhone();
}
if(price > 50000) {
return new Galaxy();
}
return new Huawei();
}
}
아래 예제 코드를 보면, 메인 메서드에서 오로지 GradeCalculator의 정적 팩토리 메서드 of() 를 호출하여 Grade 인터페이스 타입의 객체를 반환할 뿐이지, Grade 인터페이스의 구현체인 A ~ F 객체 존재에 대해서는 모르게 된다.
즉, 구현체를 생성해서 반환할 책임은 정적 팩토리 메서드를 가진 GradeCalculator 이고, 클라이언트는 구현체를 신경쓸 필요없이 제공되는 메서드를 호출만 하면 되어 편리하게 사용이 가능해진다.
interface Grade {
String toText();
}
class A implements Grade {
@Override
public String toText() {return "A";}
}
class B implements Grade {
@Override
public String toText() {return "B";}
}
class C implements Grade {
@Override
public String toText() {return "C";}
}
class D implements Grade {
@Override
public String toText() {return "D";}
}
class F implements Grade {
@Override
public String toText() {return "F";}
}
class GradeCalculator {
// 정적 팩토리 메서드
public static Grade of(int score) {
if (score >= 90) {
return new A();
} else if (score >= 80) {
return new B();
} else if (score >= 70) {
return new C();
} else if (score >= 60) {
return new D();
} else {
return new F();
}
}
}
public static void main(String[] args) {
String jeff_score = GradeCalculator.of(36).toText();
String herryPorter_score = GradeCalculator.of(99).toText();
System.out.println(jeff_score); // F
System.out.println(herryPorter_score); // A
}
정적 팩토리 메서드 와 다른 정적 메서드와 역할을 구분짓기 위해 독자적인 네이밍 컨벤션(Convention)이 존재한다. 단, 정적 팩토리 메서드에서의 네이밍은 단순히 선호도 의미를 넘어서 거의 법칙 정도로 사용되는 것이니, 각 네이밍의 역할에 대해 알아두는 것은 개념을 아는 것만큼 중요하다. (이를 왜 지켜야 되는지는 정적 팩토리 메서드 문제점 색션에서 후술한다)
정적 팩토리 메서드에서 사용되는 네이밍 단어 종류는 다음과 같다.
from : 하나의 매개 변수를 받아서 객체를 생성
of : 여러개의 매개 변수를 받아서 객체를 생성
getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음
newInstance | create : 항상 새로운 인스턴스를 생성
get[OrderType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음
new[OrderType] : 항상 다른 타입의 새로운 인스턴스를 생성
// from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Date d = Date.from(instant);
// of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
// valueOf : from과 of의 더 자세한 버전
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
// instance 혹은 getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
StackWalker luke = StackWalker.getInstance(options);
Calendar instance = Calendar.getInstance();
// create 혹은 newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
Object newArray = Array.newInstance(classObject, arrayLen);
// getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.
FileStore fs = Files.getFileStore(path);
// newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.
BufferedReader br = Files.newBufferdReader(path);
// type : getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);
따라서 클래스 설계자는 API 문서를 깔끔하게 작성할 필요가 있으며, 정적 팩토리 메서드를 작성할때 네이밍 컨밴션을 지킴으로써 문제점을 극복하기도 한다.
다만, 이 부분은 단점이라기 보단 스펙에 가깝다고 얘기할수 있다.
대표적인 예로 자바의 Collections 클래스를 보면 생성자가 private로 되어 있다. 이는 일부로 상속을 하게 하지 않기 위한 설계된건데, Collections는 자바의 컬렉션에 대한 헬퍼 제공 용도이지 상속을 통해 무언가를 확장하게 하기 위한 용도가 아니기 때문이다. 따라서 이러한 제약은 상속(Inheritance) 보단 합성(Composition)을 사용하도록 유도하게 하거나, 불변(Immutable) 객체로 만들고 싶을때 사용되는 하나의 코드 패턴이라고 보면 된다.
따라서 클래스 설계자는 API 문서를 깔끔하게 작성할 필요가 있으며, 정적 팩토리 메서드를 작성할때 네이밍 컨벤션을 지킴으로써 문제점을 극복하기도 한다.