우리가 보편적으로 클래스를 만들어 인스턴스를 생성하기 위해서 사용하는 것은 public 생성자이다. 하지만 우리는 public 생성자 말고도 인스턴스를 생성하기 위해 사용할 수 있는 기법이 존재한다. 바로 정적 팩터리 메서드를 사용하는 것이다.
그렇다고 해서 디자인 패턴의 팩토리 메서드 패턴등을 사용하라는 것이 아니다. 그저 클래스의 인스턴스를 반환하는 단순한 정적 메서드를 사용하는 것을 말한다.
이 기법에는 5가지의 장점과 2가지의 단점이 존재한다. 이에 대해서 살펴보자.
생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 하지만, 정적 팩터리 메서드를 만들어 사용하면 반환될 객체의 즉성을 설명할 수 있다. 아래의 예시를 살펴보자.
우리가 만약 이름, 나이, 키를 필드로 가진 클래스 Human
이 있다고 가정해보자.
public class Human {
private String name;
private int age;
private int height;
}
이때 우리는 나이
와 이름
만을 가지는 Human
과 키
와 이름
만을 가지는 Human
을 각각 만들고 싶어 각각의 생성자를 아래와 같이 만들고 싶다고 생각해보자.
public Human(String name, int age) {
this.name = name;
this.age = age;
}
public Human(String name, int height) {
this.name = name;
this.height = height;
}
위와 같은 경우 컴파일 에러가 뜬다. 하나의 시그니처로는 생성자를 하나만 만들 수 있는데, 두 생성자의 시그니처가 같기 때문이다. 매개변수의 이름만 다른 것은 영향을 줄 수 없다. 따라서 우리는 위에서 우리가 하고 싶었던 Human
둘 중 하나만을 선택해야 한다. 하지만, 정적 팩터리 메서드를 사용하면 이를 해결할 수 있다.
public Human(String name, int age, int height) {
this.name = name;
this.age = age;
this.height = height;
}
public static Human createWithAge(String name, int age) {
return new Human(name, age, 0);
}
public static Human createWithHeight(String name, int height) {
return new Human(name, 0, height);
}
이렇게 정적 팩터리 메서드를 만들어 createWithAge
, createWithHeight
를 각각 사용하면 우리가 원하는 타입의 Human을 각각 만들 수 있다.
불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 대표적인 예로는 Boolean.valueOf(boolean)
이 있는데 이는 객체를 생성하지 않는다. 아래에서 살펴보자.
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);
@HotSpotIntrinsicCandidate
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
}
위를 보면 이미 public final
로 생성해놓은 인스턴스인 TRUE, FALSE
를 매개변수인 b에 따라 반환해주고 있는 것을 볼 수 있다. 즉, 정적 팩터리 메서드를 사용해 인스턴스를 매번 생성하는 것이 아닌 미리 생성해놓고 필요할 때 마다 가져다 쓰는 것을 볼 수 있다.
이는 생성 비용이 큰 같은 객체가 자주 요청 될 때 성능을 올려줄 수 있다.
자주 사용하는 인스턴스들을 매번 생성하는 것이 아닌 미리 캐싱해놓고 꺼내 쓰는 디자인 패턴인 플라이웨이트 패턴과도 유사하다.
반복되는 요청에 같은 객체를 반환하는 식으로 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있었는데, 이를 인스턴스 통제라고 한다. 즉, 내가 인스턴스를 생성하고 싶을 때 생성할 수 있는 것을 말한다.
그렇다면 왜 인스턴스 통제를 사용할까?
생성자는 호출될 때마다 new
연산자를 사용해 인스턴스를 생성한다. 항상 인스턴스를 생성하기 때문에 생성자로는 인스턴스를 통제할 수 없다.
메서드에의 리턴 타입은 인터페이스, 반환은 상황에 맞는 구현체로 할 수 있는 것이다. 물론, 인터페이스가 아닌 클래스를 사용해 상속받은 클래스를 반환할 수 있다. 이를 통해 유연성을 가지게 된다.
생성자는 생성하는 클래스의 인스턴스만 반환 가능하기 때문에 이러한 유연성이 불가능하다.
또한 이를 통해 클라이언트에게 구현체를 숨길 수도 있는 것이다. 아래의 예제를 살펴보자.
public interface Human {
}
public class Man implements Human {
private String name;
private int age;
private int height;
public Man(String name, int age, int height) {
this.name = name;
this.age = age;
this.height = height;
}
public static Human createWithAge(String name, int age) {
return new Man(name, age, 0);
}
}
public class Main {
public static void main(String[] args) {
Human human = Man.createWithAge("effective java", 3);
}
}
Human
이라는 인터페이스가 있고, Man
이라는 클래스가 존재할 때, Man에서는 Human
을 메서드의 반환타입으로 사용해 반환할 수 있다. 이를 main
메서드에서 적절히 사용했음을 볼 수 있다.
java 8부터는 인터페이스에 static 메서드를 구현해 바로 사용할 수 있으므로 아래와 같은 예제도 생각해볼 수 있다.
public class Man implements Human {
private String name;
private int age;
private int height;
public Man(String name, int age, int height) {
this.name = name;
this.age = age;
this.height = height;
}
public static Man createWithHeight(String name, int height) {
return new Man(name, 0, height);
}
}
public interface Human {
public static Human create() {
return Man.createWithHeight("test", 3);
}
}
public class Main {
public static void main(String[] args) {
Human human = Human.create();
}
}
위의 예제를 보면 Main에서 클라이언트는 Human
이라는 인터페이스만 알 뿐, 구현체인 Man
에 대해서는 알 수 없다.
위의 장점에서 보았던 Human
을 응용해보자.
public class Woman implements Human{
private String name;
private int age;
private Woman(String name, int age) {
this.name = name;
this.age = age;
}
public static Woman createWithAge(String name, int age) {
return new Woman(name, age);
}
}
public class Man implements Human {
private String name;
private int age;
private int height;
private Man(String name, int age, int height) {
this.name = name;
this.age = age;
this.height = height;
}
public static Man createWithHeight(String name, int height) {
return new Man(name, 0, height);
}
}
Human
을 구현(implement
)한 Woman
과 Man
클래스를 각각 만들어주었다. 그리고 Human
에서 매개변수인 gender
에 따라 둘 중 한 클래스를 반환하도록 했다.
public interface Human {
public static Human create(String gender) {
if (gender.equals("female")) {
return Woman.createWithAge("woman", 25);
}
if (gender.equals("male")) {
return Man.createWithHeight("test", 3);
}
throw new IllegalArgumentException();
}
}
위 처럼 매개변수에 따라 Woman
클래스를 반환할 수도, Man
클래스를 반환할 수도 있는 것이다.
사용할 때는 이전의 장점과 마찬가지로 클라이언트는 Woman
을 구현한 것인지 Man
을 구현한 것인지 모른다.
public class Main {
public static void main(String[] args) {
Human human = Human.create("female");
}
}
클라이언트는 Human
만을 볼 수 있기 때문이다.
JDBC
를 생각해보자. 우리는 사용할 데이터베이스를 미리 알고 코드를 작성하는 경우도 있지만, 사용할 데이터베이스를 모른채로 코드를 작성하는 경우가 있을 수 있다. 이때 특정 데이터베이스에 특화되지 않도록 driver만 바꿔 끼우면 동작할 수 있도록 도와주는 것이 JDBC
이다.
JDBC
는 Class.forName(String className)
을 통해 해당 클래스를 JVM에 로드하게 된다. 클래스 로딩 프로세스는 드라이버 인스턴스를 DriverManager에 등록하고 이 클래스를 oracle 또는 postgres와 같은 데이터베이스 엔진 식별자와 연결하는 정적 초기화 루틴을 촉진한다.
Class.forName("oracle.jdbc.driver.OracleDriver");
그 후 아래와 같은 방식으로 드라이버를 등록한다.
public static void register() throws SQLException {
if (isRegistered()) {
throw new IllegalStateException("Driver is already registered. It can only be registered once.");
} else {
Driver registeredDriver = new Driver();
DriverManager.registerDriver(registeredDriver);
Driver.registeredDriver = registeredDriver;
}
}
따라서 클래스를 생성할 땐 어떤 Driver가 들어오는지 모르지만, 사용자가 어떤 Driver를 입력 값에 넣느냐에 따라 실행시점에 Driver이름 (class name)을 통해 클래스를 가져와 인스턴스를 초기화시킨다.
하지만 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아 들일 수도 있다.
생성자처럼 정해져 있는 포맷이 없으므로 사용자는 클래스에서 정적 팩터리 메서드를 일일이 찾아야 한다. 따라서 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 지어 이 문제를 완화할 수 있다.
메서드 명 | 설명 |
---|---|
from | 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드 |
of | 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드 |
valueOf | from과 of의 더 자세한 버전 |
instance 혹은 getInstance | (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다. |
create 혹은 newInstance | instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다. |
getType | getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. (Type은 팩터리 메서드가 반환할 객체의 타입 ex. FileStore fs = Files.getFileStore(path)) |
newType | newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. (Type은 팩터리 메서드가 반환할 객체의 타입 ex. BufferedReader br = Files.newBufferedReader(path)) |
type | getType과 newType의 간결한 버전 |
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니, 상대적인 장단점을 이해하고 사용하는 것이 좋다.
정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.
Loading JDBC Drivers
이펙티브 자바 3/E