정적 팩터리 메서드는 이름을 가질 수 있다.
클래스의 인스턴스를 얻는 기본적인 방법은 public생성자를 이용하는 것이다. 하지만 생성자를 이용하는 방식은 자유로운 인스턴스의 생성을 방해하기도 한다.
예를 들어 게임 캐릭터를 생성하는데 이벤트 유저와, 일반 유저를 boolean으로 구분한다고 생각해보자.
public class User {
private boolean event;
private boolean normal;
private GameCharacter gameCharacter;
public User(boolean event, GameCharacter gameCharacter) {
this.event = event;
this.gameCharacter = gameCharacter;
}
public User(GameCharacter gameCharacter, boolean normal) {
this.normal = normal;
this.gameCharacter = gameCharacter;
}
}
생성자는 같은 매개 변수를 갖는다면 하나의 생성자만 가질 수 있다. 그래서 다음과 같이 매개 변수의 순서를 바꾸는 꼼수를 사용할 수 있다. 이는 각 생성자가 어떤 역할을하는지 알 수 없어 생성자를 잘못 호출할 가능성이 있다. 이를 위해 정적 팩토리 메서드를 고려해볼 수 있다.
public static User eventUser(GameCharacter gameCharacter) {
User user = new User();
user.event = true;
user.gameCharacter = gameCharacter;
return user;
}
다음과 같이 코드를 작성하면 이벤트 유저를 생성한다는 것을 쉽게 알 수 있다. 즉, 한 클래스에 시그니처가 같은 생성자가 여러 개 필요하다면, 생성자를 정적 팩토리 메서드로 바꾸고 각각의 차이를 잘 보여주는 이름을 지어주는 것이 좋다.
불필요한 객체 생성을 피할 수 있다.
생성자를 이용해서 인스턴스를 생성하게 되면 매번 새로운 인스턴스를 생성하게 된다. 하지만 정적 팩터리 메서드를 사용하게 되면 이러한 객체 생성을 컨트롤할 수 있다.
public class GameCharacter {
private String username;
private int level;
private GameCharacter() {};
private static final GameCharacter INSTANCE = new GameCharacter();
public static GameCharacter newInstance() {
return INSTANCE;
}
}
이렇게 정적 팩터리 방식의 클래스는 언제 어느 인스턴스가 살아 있게 할지를 통제할 수 있다. 그렇다면 왜 인스턴스를 통제해야 할까?
- 싱글톤 기법 : 인스턴스를 하나만 생성하고 모든 곳에서 자유롭게 공유하여 사용할 수 있다.
- 인스턴스화가 불가능하게 만들 수 있다
-> private 생성자를 사용함으로써 new()를 이용한 인스턴스 생성이 불가능하다.
- 불변 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.
반환 타입을 하위 타입의 객체로 반환할 수 있다 / 입력 매개 변수에 따라 다른 클래스 객체를 반환할 수 있다.
반환 타입을 자유롭게 선택할 수 있는 유연성을 제공한다. 특히 자바8이후부터는 interface에 정적 메서드를 선언할 수 있게 되어 더 편리해다.
public interface LanguageService {
String hello();
static LanguageService of(String lang) {
if(lang.equals("ko")) return new KoreanLanguageService();
else return new FrenchLanguageService();
}
}
public class FrenchLanguageService implements LanguageService {
@Override
public String hello() {
return "Bonjour";
}
}
public class KoreanLanguageService implements LanguageService {
@Override
public String hello() {
return "안녕하세요";
}
}
public class LanguageMain {
public static void main(String[] args) {
LanguageService languageService2 = LanguageService.of("ko");
System.out.println(languageService2.hello());
}
}
정적 팩터리 메서드를 작성하는 시점에 반활할 객체의 클래스가 존재하지 않아도 된다.
ServiceLoader클래스를 이용하면 인터페이스를 상속하고 있는 모든 클래스를 런타임에 가지고 올 수 있다. 즉, 런타임 시점에 실행할 클래스를 선택할 수 있다.
이는 서비스 제공자 프레임워크의 기반이 되기도 한다.
[서비스 제공자 프레임워크]
서비스 제공자 프레임워크는 3개 핵심 컴포넌트로 이루어져있으며, 추가적으로 하나가 더 이용되기도 한다. DB 커넥션을 예시로 이해해보자 (코드는 JDBC 코드를 보면서 임의로 수정한 것이다.)
- 서비스 인터페이스 : 구현체의 동작을 정의한다.
public interface Connection {
Statement createStatement() throws SQLException;
PreparedStatement prepareStatement(String sql) throws SQLException;
...
}
- 제공자 등록 API : 제공자가 구현체를 등록할 때 사용한다.
public class DrvierManager {
private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static void registerDriver(java.sql.Driver driver,DriverAction da) throws SQLException {
/* Register the driver if it has not already been added to our list */
if (driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
}
- 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API
public class DriverManager {
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
}
정적 팩터리 메서드의 단점
- 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
=> 상속을 하기 위해서 public, protected 생성자를 제공해야 하기 때문이다.
=> 정적 팩터리 메서드 사용을 강제하기 위해 private 생성자를 사용하는 것이 일반적이다. - 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
=> 생성자처럼 API 설명에 명확히 드러나지 않아 프로그래머는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 찾아내야 한다.
=> 이를 해결하기 위해 정적 팩터리 메서드에 흔히 사용하는 명명 방식이 있다.
// 1. from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Date d = Date.from(instant);
// 2. of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EumSet.of(JACK, QUEEN, KING);
// 3. valueOf : from과 of의 더 자세한 버전
BigInteger prime = BingInteger.valueOf(Integer.MAX_VALUE);
// 4. instace 혹은 getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
GameCharacter gameChacter = GameCharacter.getInstance(option);
// 5. create 혹은 newInstance : 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
Object newArray = Array.newInstance(classObject, arrayLen);
// 6. getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반활할 객체의 타입을 적는다.
FileStore fs = Files.getFileStore(path);
// 7. newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.
BufferedReader br = Files.newBufferedReader(path);
// 8. type : getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);