[Java] 이팩티브 자바 - #1. 생성자 대신 static 팩토리 메서드를 고려해라
생성자에 비해 static 메서드가 가지는 유연함에 대한 글.
장점 1. 이름을 가질 수 있다.
생성자에 제공하는 파라미터가 해당 클래스(반환값)를 잘 대변하지 못하는 경우 메서드 명을 가질 수 있는 static 메서드가 더 유용할 수 있다. 또한 똑같은 타입을 매개변수로 받는 생성자는 두 개 이상 존재할 수 없기 때문에 그런 경우에도 이름을 갖는 메서드가 낫다.
/**
* 생성자를 이용하여 에러가 발생하는 코드
*/
public class Foo {
String name;
String address;
public Foo(String name){
this.name = name;
}
public Foo(String address){
this.address = address;
} // 에러.
}
/**
* static 메서드를 이용해서 해결
*/
public class Foo {
String name;
String address;
public Foo(){}
public Foo(String name){
this.name = name;
}
public static Foo withName(String name){
return new Foo(name);
}
public static Foo withAddress(String address){
Foo foo = new Foo();
foo.address = address;
return foo;
}
}
장점 2. 반드시 새로운 객체를 만들 필요가 없다.
불변 클래스인 경우나 매번 새로운 객체를 만드는 것이 아닌 경우에 미리 만들어둔 인스턴스 또는 캐시해둔 인스턴스를 반환할 수 있다.
장점 3. 리턴 타입의 하위 타입 인스턴스를 만들 수도 있다.
메서드 선언부에서 리턴타입과 달리 실제 리턴 타입에는 하위 클래스가 올 수도 있다.
/**
* getFoo 메서드에서 선언부에는 Foo, 실제 반환값에는 BarFoo(Foo의 하위클래스)를 포함시킴
*/
public class Foo {
String name;
String address;
public static Foo getFoo(boolean flag){
return flag ? new Foo() : new BarFoo();
}
public static void main(String[] args) {
Foo foo = Foo.getFoo(true);
}
static class BarFoo extends Foo{
}
}
혹은 선언부에는 인터페이스만 노출하고 실제 리턴 객체에는 구현체가 올 수 있다. 이렇게 하면 메서드를 사용하는 입장(클라이언트)에서는 인터페이스만 가지고 코딩을 할 수 있다. 예를 들면 java.utils.Collections 가 그렇게 되어 있다. Collections에는 45개의 인터페이스 구현체가 non-public으로 제공된다. 따라서 45개의 구현체가 인터페이스 뒤에 숨어 있기 때문에 우리가 알아야 하는 개념(각 구현체를 사용할 때의 개념)의 무게도 줄었다.
자바8 부터는 인터페이스에 public static 메서드를 추가할 수 있게 되었고, 자바9부터는 private static 메서드를 추가할 수 있게 되었다. 따라서 자바8을 사용하면서 인터페이스에 public static을 이용해서 그 인터페이스의 구현체 메서드를 제공할 수도 있지만 private static 메서드가 필요한 경우는 기존처럼 별도의 클래스를 사용해야 할 수도 있다. -> 예를 들면 Collections가 가지고 있는 메서드들을 Collection(인터페이스)에 둬도 된다는 것. (public static 메서드를 사용하면 body가 있어야 하고 구현체에서 오버라이딩이 불가하니까)
장점 4. 리턴하는 객체의 클래스가 입력 매개변수에 따라 매번 다를 수 있다.
장점 3과 같은 이유로 객체의 타입은 다를 수 있다. EnumSet 클래스는 생성자 없이 public static 메소드, allOf(), of() 등을 제공한다. 그 안에서 리턴하는 객체의 타입은 enum 타입의 개수에 따라 RegularEnumSet 또는 JumboEnumSet으로 달라진다.
그런 객체 타입은 노출하지 않고 감춰져 있기 때문에 추후에 JDK의 변화에 따라 새로운 타입을 만들거나 기존 타입을 없애도 문제가 되지 않는다.
장점 5. 리턴하는 객체의 클래스가 public static 팩토리 메서드를 작성하는 시점에 반드시 존재하지 않아도 된다.
장점 3, 4와 비슷한 개념으로 유연성을 제공하는 static 팩토리 메서드는 서비스 프로바이더 프레임워크의 근본이다. 대표적인 예시가 JDBC가 된다. (참고 : 서비스 프로바이더 프레임워크란?)
서비스 프로바이더 프레임워크는 다음의 것들을 필요로 한다.
- 서비스의 구현체를 대표하는 서비스 인터페이스
- 구현체를 등록하는데 사용하는 프로바이더 등록 API
- 클라이언트가 해당 서비스의 인스턴스를 가져갈 때 사용하는 서비스 엑세스 API
JDBC의 경우 DriverManager.registerDriver()가 프로바이더 등록 API, DriverManager.getConnection()이 서비스 엑세스 API, 그리고 Driver가 서비스 프로바이더 인터페이스 역할을 한다.
단점 1. public 또는 protected 생성자 없이 static public 메소드만 제공하는 클래스는 상속할 수 없다.
따라서 Collections 프레임워크에서 제공하는 편의성 구현체 (java.util.Collection)는 상속할 수 없다. 하지만 오히려 불변 타입인 경우나 상속 대신 컴포지션을 권장하기 때문에 단점이라고 보기 어려울 수 있다.
단점 2. 프로그래머가 static 팩토리 메서드를 찾는 게 어렵다.
javadoc 상단에 생성자는 모아서 보여주는 반면 static 팩토리 메서드들은 API 문서에서 따로 다루지 않는다. 따라서 클래스나 인터페이스 문서 상단에 팩토리 메서드에 대한 문서를 제공하는 것이 좋다.