정적 팩토리 메서드 (Static factory method)
정적 팩토리 메서드란 간단히 말해 객체를 생성할 때, 생성자를 쓰지 않고 정적 메서드를 사용하는 것이다.
디자인 패턴의 팩토리 메서드와는 다른 것이며, 디자인 패턴 중에는 정적 팩토리 메서드와 일치하는 것은 존재하지 않는다.
이펙티브 자바에 나오는 정적 팩토리 메서드의 장단점을 쉽게 이해하기 위해 예제 코드를 함께 기술하였으며, 이것은 무조건적인 정답이 아니기 때문에 직접 예제 코드를 만들어보는 것도 좋은 방법이다. 개인적으로는 해당 기법을 처음 보는 사람이라면 장점 1, 2, 4번과 단점만을 이해하여도 충분히 잘 사용할 수 있을 것이라 생각한다.
생성자의 접근 제어자가 public인 경우, 생성자를 통해 객체 생성을 언제 어디서든 제한 없이 할 수 있고, 어떤 인스턴스를 반환할 것인지 제어할 수 없게 된다. 정적 팩토리 메서드를 사용하면 객체 생성을 자기 자신이 관리할 수 있다. 정적 팩토리 메서드와 public 생성자는 각자의 장단점이 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
장점
이름을 가질 수 있다.
public class Book {
String name;
String author;
String translator;
String publisher;
public Book(String name, String author, String publisher) { // Compile Error !
this.name = name;
this.author = author;
this.publisher = publisher;
}
public Book(String name, String translator, String publisher) { // Compile Error !
this.name = name;
this.translator = translator;
this.publisher = publisher;
}
}
Book 클래스에서 첫 번째 생성자와 두 번째 생성자는 반환 타입, 매개 변수의 개수와 타입이 모두 같으므로 동일한 시그니처의 생성자이다. 따라서 이미 정의된 생성자라고 컴파일 오류가 발생하는데, 정적 팩토리 메서드를 사용하여 이를 해결할 수 있다. 각 정적 팩토리 메서드는 이름을 다르게 가질 수 있기 때문에 가능하다. 아래의 예제는 정적 팩토리 메서드 방식으로 바꾼 것이다.
public class Book {
String name;
String author;
String translator;
String publisher;
public static Book domesticBook(String name, String author, String publisher) {
Book book = new Book();
book.name = name;
book.author = author;
book.publisher = publisher;
return book;
}
public static Book foreignBook(String name, String translator, String publisher) {
Book book = new Book();
book.name = name;
book.translator = translator;
book.publisher = publisher;
return book;
}
}
이와 같은 방식으로 정적 팩토리 메서드를 활용할 수 있다.
아래와 같이 캐릭터를 나타내는 클래스가 있다고 가정한다. 생성자 사용 시 캐릭터 스텟 값으로 숫자만 들어가므로 어떤 캐릭터인지 직관적으로 알기 힘들다. 정적 팩토리 메서드를 사용해 이름을 부여하면 어떤 캐릭터인지 직관적으로 알 수 있다.
public class Character {
int strength, agility, luck, intellect;
public Character(int strength, int agility, int luck, int intellect) {
this.strength = strength;
this.agility = agility;
this.luck = luck;
this.intellect = intellect;
}
public static Character warrior() { // 정적 팩토리 메서드
return new Character(8, 8, 4, 4);
}
public static Character archer() { // 정적 팩토리 메서드
return new Character(4, 8, 8, 4);
}
public static Character thief() { // 정적 팩토리 메서드
return new Character(8, 4, 8, 4);
}
public static Character mage() { // 정적 팩토리 메서드
return new Character(4, 4, 8, 8);
}
}
public class Main {
Character myCharacter = new Character(8, 4, 8, 4); // 이게 무슨 캐릭터?
Character warrior = Character.warrior();
Character mage = Character.mage();
}
Main 클래스에서는 정적 팩토리 메서드인 warrior()와 mage() 메서드를 사용하여 인스턴스를 생성하였다. 해당 메서드들을 사용하여 전사 캐릭터를 생성할 것인지, 마법사 캐릭터를 생성할 것인지 알 수 있다.
호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
이펙티브 자바 책에선 본 장점을 설명할 때 플라이웨이트 패턴이 언급된다. 플라이웨이트 패턴이란 자주 사용하는 값을 미리 캐싱해서 넣어두고, 거기서 꺼내서 사용하는 것이다. 해당 장점은 플라이웨이트 패턴과 통용되는 개념이다.
생성자를 사용해 객체를 생성하기 위해서는 new 생성자()를 해야 하는데, 이때마다 인스턴스를 새로 생성한다. (객체와 인스턴스는 다른 개념이다. 참고) 하지만 정적 팩토리 메서드를 사용하면 기존 인스턴스를 반환하는 방식으로 객체를 생성할 수 있다.
이 방법을 통하여 같은 객체를 자주 요청할 때 성능을 끌어올릴 수 있다. 같은 객체를 여러 번 생성하지 않아도 되니 메모리 효율 측면에서 좋다. 또한 반복되는 요청에 같은 객체를 반환하는 방법을 이용해, 언제 어느 인스턴스를 살아 있게 할 것인지 통제할 수 있다.
Boolean.valueOf() 메서드가 대표적인 예시이다.
public final class Boolean implements java.io.Serializable, Comparable<Boolean> {
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
// 생략
}
Boolean 클래스에서 true와 false 값은 자주 사용되므로 TRUE, FALSE 인스턴스를 먼저 생성해 놓는다. valueOf() 메서드를 사용할 때마다 기존에 생성해 두었던 인스턴스인 TRUE 또는 FALSE를 반환해 준다.
반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
해당 장점은 정적 팩토리 메서드를 사용하면, 인터페이스 타입을 사용하여 객체를 선언해도 인터페이스를 구현한 구현체 타입 객체를 반환해 줄 수 있다. 는 의미이다. B 클래스가 A 인터페이스의 구현체라면, B 클래스 타입을 반환해도 A 인터페이스 타입으로 사용할 수 있다는 것이다. 아래 블로그가 그 예시를 가장 잘 나타내준다.
이런 방식이 가능한 경우 반환할 객체의 클래스-사용할 인터페이스를 구현한 클래스-를 자유롭게 선택할 수 있기 때문에 유연함이라는 장점이 생긴다. 인터페이스로 감쌈으로써 클라이언트에는 정확히 어떤 타입을 반환하는지 숨길 수 있다. 따라서 클라이언트는 구현 클래스가 아닌 인터페이스만으로 객체를 다룰 수 있다.
처음에는 인터페이스만으로 객체를 다룰 수 있다는 게 대체 뭐가 장점인가 생각했다. 책에서는 자바 컬렉션 프레임워크를 예시로 든다. 자바 컬렉션 프레임워크는 핵심 인터페이스들에 수정 불가나 동기화 등의 기능을 덧붙인 총 45개의 유틸리티 구현체를 제공하는데, 이 구현체 대부분을 java.util.Collections 클래스에서 정적 팩토리 메서드를 통해 얻도록 했다. 또한 컬렉션 프레임워크는 구현체를 직접적으로 공개하지 않는다.
수정 불가나 동기화 기능을 사용하고 싶을 때 이 기능을 어떤 구현체 클래스에서 제공하는지 살펴보고, 해당 기능을 제공하는 클래스의 인스턴스를 생성하여 사용할 필요가 없다. 즉, 45개의 기능을 사용하기 위해 45개의 유틸리티 구현체를 직접 하나하나 살펴보고 인스턴스를 생성할 필요가 없어진 것이다. 정적 팩토리 메서드로 구현되지 않았다면 45개의 기능을 사용하기 위해 각각의 기능이 어떻게 구현되어 있는지를 살펴봐야 했을 것이다. 또한 45개 중 어떠한 기능을 쓰기 위해서, 해당 기능이 구현되어 있는 클래스 인스턴스를 선언하여 사용했을 것이다. 하지만 정적 팩토리 메서드로 구현되어 있으므로 Collections에서 저런 기능을 제공하는구나만 대략적으로 알고, Collection-인터페이스- 인스턴스를 사용하기만 하면 된다.
참고로 Collections는 인터페이스가 아니라 클래스인데, 이는 Collection이 자바 7 버전에 만들어졌기 때문이다. 인터페이스에서 정적 메서드를 지원하는 건 자바 8 버전부터 이다. 자바 8 버전 이전에 만들어진 Collection 인터페이스에 정적 메서드가 필요하면 Collections라는 인스턴스화 불가인 동반 클래스를 만들고, 여기에 정적 메서드를 정의했다. 그러므로 인스턴스를 생성할 때는 Collections가 아닌, Collection 인터페이스를 사용하여 생성하는 것이다.
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
public class Student {
int age;
public static Student of(int age) throws Exception {
if (age >= 8 && age < 14) {
return new Elementary();
} else if (age >= 14 && age < 17) {
return new Middle();
} else if (age >= 17 && age < 20) {
return new High();
} else {
throw new Exception("학생이 아님");
}
}
}
class Elementary extends Student {}
class Middle extends Student {}
class High extends Student {}
Student 클래스가 있다고 가정한다. Elementary 클래스와 Middle 클래스, High 클래스는 Student 클래스를 상속받는 클래스이다. 정적 팩토리 메서드인 of() 메서드에 age 인자 값에 어떤 값이 들어오냐에 따라 반환해 주는 클래스가 다르다.
public class Main {
try {
Student person1 = Student.of(12);
Student person2 = Student.of(17);
System.out.println(person1.getClass().getSimpleName());
System.out.println(person2.getClass().getSimpleName());
} catch (Exception e) {
e.printStackTrace();
}
}
이를 실행해 보면 person1의 클래스는 Student가 아닌 Student 하위 타입인 Elementary이고, person2의 클래스도 마찬가지로 Student의 하위 타입인 High임을 확인할 수 있다.
지금은 getClass().getSimpleName() 메서드를 사용해 클래스 이름을 가져왔기 때문에 person1과 person2가 각각 어떤 클래스의 인스턴스인지 알 수 있었다. 이처럼 직접적으로 클래스 이름을 가져오지 않는 이상, 클라이언트는 정적 팩토리 메서드가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수 없으며 알 필요도 없다. 그저 Student 클래스의 하위 클래스이기만 하면 된다. 사실상 클라이언트는 Elementary와 High 클래스의 존재를 모른다.
대표적 예시로 EnumSet.noneOf() 메서드가 해당 장점을 이용하는 정적 팩토리 메서드이다.
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");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
universe의 length가 64 이하이냐, 초과이냐에 따라 반환하는 인스턴스가 다르다.
정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
책에서 해당 장점은 서비스 제공자 프레임워크(Service provider framework)를 만드는 근간이 된다고 소개한다. JDBC-대표적인 서비스 제공자 프레임워크-를 예시로 들어 설명했다. (사실 책 내용만 보면 전혀 이해가 안 가지만 ServiceLoader를 사용하여 구현한 예제를 보면 이해가 간다..)
Java와 Database를 연결할 때 JDBC를 사용한다. 우리는 JDBC를 사용해 MySQL이든 Oracle이든 MariaDB든 그때그때 필요한 데이터베이스와 연결해서 사용할 수 있다. 만약 JDBC라는 서비스 제공자 프레임워크라는 것이 없었다면, 각각의 데이터베이스를 연결할 때마다 필요한 코드를 직접 구현해야 했을 것이며, 각기 다른 데이터베이스에 대한 인스턴스를 생성해주어야 했을 수도 있다.
JDBC가 없었다면 MySQL과 연동하기 위해 MySQL 연동용 코드-클래스-를 작성하고, MySQL 클래스에 대한 인스턴스를 생성해 사용해야 했을 것이며, MongoDB와 연동하기 위해 (MySQL 연동 코드와는 다른) MongoDB 연동 코드를 작성하여 MongoDB 클래스 인스턴스를 생성해 사용해야 했을 것이다. 연결하려는 데이터베이스에 따라 각각의 연동 로직을 직접 작성하고, 새로운 인스턴스를 생성해야 하는 것이다.
JDBC를 이용하면 그냥 사용할 데이터베이스 Driver를 명시해 주고 Connection만 해주면 끝이었다. 이에 관한 예제는 다음의 블로그에 잘 나타나있다.
자바 6부터는 ServiceLoader를 제공하기 때문에 이를 이용해 본 장점을 편하게 직접 구현할 수 있다. ServiceLoader는 load()라는 정적 팩토리 메서드를 제공한다. 이를 사용해 인자로 들어온 클래스를 구현하는 구현체 중, 등록된 구현체를 가져올 수 있다. 단순히 ServiceLoader는 서비스 제공자 프레임워크를 구현하는 구현체 정도로 이해하면 된다. load() 메서드가 서비스 접근 API 역할을 한다. 여담으로 JDBC는 ServiceLoader가 나오기 이전의 기술이기 때문에 ServiceLoader를 사용하지 않고 구현되어 있다.
단점
정적 팩토리 메서드'만' 제공하면 상속을 못 할 수 있다.
장점에서 봤던 예제 코드들은 생성자를 public으로 만들었거나, 기본 생성자가 정적 팩토리 메서드와 함께 존재했기 때문에 상속을 할 수 있었다. 자바에서 생성자와 정적 팩토리 메서드를 동시에 제공하는 대표적 예시로는 List가 있다.
생성자를 제공하지 않고, 오로지 정적 팩토리 메서드만을 사용해 인스턴스를 반환하는 클래스를 만들고 싶은 경우, 해당 클래스의 생성자의 접근 제어자는 private으로 해야 한다. 그래야 생성자를 사용해 인스턴스를 반환하는 것을 막아 정적 팩토리 메서드만을 사용하게끔 할 수 있기 때문이다. 상속을 하기 위해서는 public이나 protected가 붙은 생성자가 있어야 하나, 그러한 생성자가 없으므로 정적 팩토리 메서드만을 제공하는 클래스는 상속을 하지 못한다.
정적 팩토리 메서드는 프로그래머가 찾기 어렵다.
JavaDoc을 직접 생성할 수 있다. 인텔리제이 기준 JavaDoc 플러그인을 설치하여 생성하면 된다. 첫 번째 장점 예제 중 하나인 Character 클래스를 JavaDoc으로 살펴본다.
생성자는 Constructor 부분에 작성되고 정적 팩토리 메서드의 경우는 Method 부분에 작성된다. 현재 예시는 메서드가 오로지 4가지뿐이고, 모두 정적 팩토리 메서드이기 때문에 찾는 게 어렵지 않을 수 있다. 만약 이보다 다양한 기능을 하는 메서드가 많다면 인스턴스를 생성해 주는 용도의 정적 팩토리 메서드가 어떤 것인지 찾기 어려워진다. 따라서 정적 팩토리 메서드 같은 것은 주석-JavaDoc에선 @See를 사용하여 나타낼 수 있다- 또는 문서화를 통해 설명을 달아놓는 게 좋다. 물론 정적 팩토리 메서드 네이밍 패턴을 사용하여 정적 팩토리 메서드임을 알아보는 데에 도움을 줄 수 있다.
정적 팩토리 메서드 네이밍 규칙
from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Ex) Date d = Date.from(instant);
of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Ex) set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf : from과 of의 더 자세한 버전
Ex) BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance 혹은 getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
Ex) StackWalker luke = StackWalker.getInstance(options);
create 혹은 newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
Ex) Object newArray = Array.newInstance(classObject, arrayLen);
getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. Type은 팩토리 메서드가 반환할 객체의 타입이다.
Ex) FileStore fs = Files.getFileStore(path);
newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. Type은 팩토리 메서드가 반환할 객체의 타입이다.
Ex) BufferedReader br = Files.newBufferedReader(path);
type : getType과 newType의 간결한 버전
Ex) List<Complaint> litany = Collections.list(legacyLitany);
보통 기존에 있는 인스턴스를 가져올 때는 get을 붙이고, 새로운 인스턴스를 생성할 때는 new 또는 create를 붙인다.
Effective Java (이펙티브 자바) - 조슈아 블로크
[인프런] 이펙티브 자바 완벽 공략 1부 - 백기선